diff --git a/generator/datamap_generator/main.py b/generator/datamap_generator/main.py
deleted file mode 100644
index 10392ea0c..000000000
--- a/generator/datamap_generator/main.py
+++ /dev/null
@@ -1,145 +0,0 @@
-import os
-import json
-from pathlib import Path
-import shutil
-
-SCRIPT_DIR = os.path.pardir
-DEFAULT_DATAMAP_PATH = os.path.join(SCRIPT_DIR, "datamaps.json")
-FOLDER = Path("../../managed/src/SwiftlyS2.Generated/Datamaps")
-
-
-MANAGER_TEMPLATE = """using SwiftlyS2.Shared.SchemaDefinitions;
-using SwiftlyS2.Core.Hooks;
-
-namespace SwiftlyS2.Core.Datamaps;
-
-internal partial class DatamapFunctionManager
-{
- public HookManager HookManager { get; }
-
-$fields$
-
- public DatamapFunctionManager(HookManager hookManager)
- {
- HookManager = hookManager;
-$constructors$
- }
-
-}
-"""
-
-
-MANAGER_FUNCTION_FIELDS_TEMPLATE = """ public BaseDatamapFunction<$className$, DHook$functionName$> $functionName$ { get; init; }
-"""
-
-MANAGER_FUNCTION_CONSTRUCTOR_TEMPLATE = """ $functionName$ = new(this, $functionHash$);"""
-
-SERVICE_TEMPLATE = """using SwiftlyS2.Shared.Datamaps;
-using SwiftlyS2.Shared.SchemaDefinitions;
-
-namespace SwiftlyS2.Core.Datamaps;
-
-internal partial class DatamapFunctionService : IDatamapFunctionService
-{
- $functions$
-}
-"""
-
-SERVICE_FUNCTION_TEMPLATE = """
- public IDatamapFunctionOperator<$className$, DHook$functionName$> $functionName$ { get; } = manager.$functionName$.Get(ctx.Name, profiler);
-
- IDatamapFunctionOperator<$className$, IDHook$functionName$> IDatamapFunctionService.$functionName$ => $functionName$;
-"""
-
-SERVICE_INTERFACE_TEMPLATE = """using SwiftlyS2.Shared.Datamaps;
-using SwiftlyS2.Shared.SchemaDefinitions;
-
-namespace SwiftlyS2.Shared.Datamaps;
-
-public partial interface IDatamapFunctionService
-{
-$functions$
-}
-"""
-
-SERVICE_INTERFACE_FUNCTION_TEMPLATE = """
- public IDatamapFunctionOperator<$className$, IDHook$functionName$> $functionName$ { get; }
-"""
-
-HOOK_CONTEXT_TEMPLATE = """using SwiftlyS2.Shared.Datamaps;
-using SwiftlyS2.Shared.SchemaDefinitions;
-
-namespace SwiftlyS2.Core.Datamaps;
-
-internal class DHook$functionName$ : BaseDatamapFunctionHookContext<$className$>, IDHook$functionName$
-{
-}
-"""
-
-HOOK_CONTEXT_INTERFACE_TEMPLATE = """using SwiftlyS2.Shared.SchemaDefinitions;
-
-namespace SwiftlyS2.Shared.Datamaps;
-
-public interface IDHook$functionName$ : IDatamapFunctionHookContext<$className$>
-{
-}
-"""
-
-def format_template(template, **params):
- for key, value in params.items():
- template = template.replace(f"${key}$", str(value))
- return template
-
-def main():
- if FOLDER.exists():
- shutil.rmtree(str(FOLDER))
- os.makedirs(FOLDER)
- os.makedirs(FOLDER / "Interfaces")
- os.makedirs(FOLDER / "Classes")
-
- f_service_interface = open(FOLDER / "Interfaces" / "IDatamapFunctionService.cs", "w", encoding="utf-8")
- f_manager = open(FOLDER / "Classes" / "DatamapFunctionManager.cs", "w", encoding="utf-8")
- f_service = open(FOLDER / "Classes" / "DatamapFunctionService.cs", "w", encoding="utf-8")
-
- with open(DEFAULT_DATAMAP_PATH, "r", encoding="utf-8") as f:
- data = json.load(f)
-
-
- datamaps = data.get("datamaps", [])
-
- manager_functions = []
- manager_constructors = []
- service_functions = []
- service_interface_functions = []
-
- for clazz in datamaps:
- class_name = clazz['class_name']
- for field in clazz["fields"]:
- if field['isFunction']:
- name = field['fieldName']
- name = name.replace("::", "_")
- hash = field['functionHash']
- manager_functions.append(format_template(MANAGER_FUNCTION_FIELDS_TEMPLATE, className=class_name, functionName=name, functionHash=hash))
- manager_constructors.append(format_template(MANAGER_FUNCTION_CONSTRUCTOR_TEMPLATE, functionName=name, functionHash=hash))
- service_functions.append(format_template(SERVICE_FUNCTION_TEMPLATE, className=class_name, functionName=name))
- service_interface_functions.append(format_template(SERVICE_INTERFACE_FUNCTION_TEMPLATE, className=class_name, functionName=name))
- hook_context_template = format_template(HOOK_CONTEXT_TEMPLATE, className=class_name, functionName=name)
- hook_context_interface_template = format_template(HOOK_CONTEXT_INTERFACE_TEMPLATE, className=class_name, functionName=name)
-
- f_hook_context = open(FOLDER / "Classes" / f"DHook{name}.cs", "w", encoding="utf-8")
- f_hook_context_interface = open(FOLDER / "Interfaces" / f"IDHook{name}.cs", "w", encoding="utf-8")
- f_hook_context.write(hook_context_template)
- f_hook_context_interface.write(hook_context_interface_template)
- f_hook_context.close()
- f_hook_context_interface.close()
-
- f_manager.write(format_template(MANAGER_TEMPLATE, fields="\n".join(manager_functions), constructors="\n".join(manager_constructors)))
- f_service.write(format_template(SERVICE_TEMPLATE, functions="\n".join(service_functions)))
- f_service_interface.write(format_template(SERVICE_INTERFACE_TEMPLATE, functions="\n".join(service_interface_functions)))
-
- f_manager.close()
- f_service.close()
- f_service_interface.close()
-
-if __name__ == "__main__":
- main()
\ No newline at end of file
diff --git a/generator/protobuf_generator/generate.py b/generator/protobuf_generator/generate.py
deleted file mode 100644
index d8e7ffcef..000000000
--- a/generator/protobuf_generator/generate.py
+++ /dev/null
@@ -1,339 +0,0 @@
-import os
-from pathlib import Path
-import sys
-from proto_schema_parser import ast, Parser
-from typing import List
-
-ROOT = Path(__file__).parent
-INPUT_DIR = ROOT.parent.parent / "protobufs" / "cs2"
-OUT_INTERFACES = ROOT.parent.parent / "managed" / "src" / "SwiftlyS2.Generated" / "Protobufs" / "Interfaces"
-OUT_CLASSES = ROOT.parent.parent / "managed" / "src" / "SwiftlyS2.Generated" / "Protobufs" / "Classes"
-OUT_ENUMS = ROOT.parent.parent / "managed" / "src" / "SwiftlyS2.Generated" / "Protobufs" / "Enums"
-
-os.makedirs(OUT_INTERFACES, exist_ok=True)
-os.makedirs(OUT_CLASSES, exist_ok=True)
-os.makedirs(OUT_ENUMS, exist_ok=True)
-
-NETMESSAGE_ENUMS = {
- "EBaseUserMessages": ("UM_", "CUserMessage"),
- "ETEProtobufIds": ("TE_", "CMsgTE"),
- "ECsgoGameEvents": ("GE_", "CMsgTE"),
- "ECstrike15UserMessages": ("CS_UM_", "CCSUsrMsg_"),
- "EBaseGameEvents": ("GE_", "CMsg"),
- "CLC_Messages": ("clc_", "CCLCMsg_"),
- "SVC_Messages": ("svc_", "CSVCMsg_"),
- "NET_Messages": ("net_", "CNETMsg_"),
-}
-
-BASE_TYPES = {
- "bool": ("Bool", "bool"),
- "int32": ("Int32", "int"),
- "sint32": ("Int32", "int"),
- "fixed32": ("UInt32", "uint"),
- "int64": ("Int64", "long"),
- "fixed64": ("UInt64", "ulong"),
- "sint64": ("Int64", "long"),
- "uint32": ("UInt32", "uint"),
- "uint64": ("UInt64", "ulong"),
- "float": ("Float", "float"),
- "double": ("Double", "double"),
- "string": ("String", "string"),
- "bytes": ("Bytes", "byte[]"),
-}
-
-MANAGED_NESTED_TYPES = {
- "CMsgVector": "Vector",
- "CMsgQAngle": "QAngle",
- "CMsgVector2D": "Vector2D",
- "CMsgRGBA": "Color",
-}
-
-FIELD_TEMPLATE = """
- public $CSTYPE$ $CSNAME$
- { get => Accessor.Get$TYPE$("$NAME$"); set => Accessor.Set$TYPE$("$NAME$", value); }
-"""
-
-ENUM_FIELD_TEMPLATE = """
- public $CSTYPE$ $CSNAME$
- { get => ($CSTYPE$)Accessor.GetInt32("$NAME$"); set => Accessor.SetInt32("$NAME$", (int)value); }
-"""
-
-NESTED_FIELD_TEMPLATE = """
- public $TYPE$ $CSNAME$
- { get => new $TYPE$Impl(NativeNetMessages.GetNestedMessage(Address, "$NAME$"), false); }
-"""
-
-REPEATED_FIELD_TEMPLATE = """
- public $CSTYPE$ $CSNAME$
- { get => new $CSTYPE_IMPL$(Accessor, "$NAME$"); }
-"""
-
-INTERFACE_FIELD_TEMPLATE = """
- public $CSTYPE$ $CSNAME$ { get; set; }
-"""
-
-INTERFACE_READONLY_FIELD_TEMPLATE = """
- public $CSTYPE$ $CSNAME$ { get; }
-"""
-
-IMPL_CLASS_TEMPLATE = """
-using SwiftlyS2.Core.Natives;
-using SwiftlyS2.Core.NetMessages;
-using SwiftlyS2.Shared.Natives;
-using SwiftlyS2.Shared.NetMessages;
-using SwiftlyS2.Shared.ProtobufDefinitions;
-
-namespace SwiftlyS2.Core.ProtobufDefinitions;
-
-internal class $NAME$ : $BASE_CLASS$<$INTERFACE$>, $INTERFACE$
-{
- public $NAME$(nint handle, bool isManuallyAllocated): base(handle)
- {
- }
-
-$FIELDS$
-}
-"""
-
-NETMESSAGE_IMPL_CLASS_TEMPLATE = """
-using SwiftlyS2.Core.Natives;
-using SwiftlyS2.Core.NetMessages;
-using SwiftlyS2.Shared.Natives;
-using SwiftlyS2.Shared.NetMessages;
-using SwiftlyS2.Shared.ProtobufDefinitions;
-
-namespace SwiftlyS2.Core.ProtobufDefinitions;
-
-internal class $NAME$ : $BASE_CLASS$<$INTERFACE$>, $INTERFACE$
-{
- public $NAME$(nint handle, bool isManuallyAllocated): base(handle, isManuallyAllocated)
- {
- }
-
-$FIELDS$
-}
-"""
-
-NETMESSAGE_INTERFACE_TEMPLATE = """
-using SwiftlyS2.Core.ProtobufDefinitions;
-using SwiftlyS2.Shared.Natives;
-using SwiftlyS2.Shared.NetMessages;
-
-namespace SwiftlyS2.Shared.ProtobufDefinitions;
-using SwiftlyS2.Shared.NetMessages;
-
-public interface $NAME$ : ITypedProtobuf<$NAME$>, INetMessage<$NAME$>, IDisposable
-{
- static int INetMessage<$NAME$>.MessageId => $MESSAGE_ID$;
-
- static string INetMessage<$NAME$>.MessageName => "$MESSAGE_NAME$";
-
- static $NAME$ ITypedProtobuf<$NAME$>.Wrap(nint handle, bool isManuallyAllocated) => new $NAME$Impl(handle, isManuallyAllocated);
-
-$FIELDS$
-}
-"""
-
-NONNETMESSAGE_INTERFACE_TEMPLATE = """
-using SwiftlyS2.Core.ProtobufDefinitions;
-using SwiftlyS2.Shared.Natives;
-using SwiftlyS2.Shared.NetMessages;
-
-namespace SwiftlyS2.Shared.ProtobufDefinitions;
-
-public interface $NAME$ : ITypedProtobuf<$NAME$>
-{
- static $NAME$ ITypedProtobuf<$NAME$>.Wrap(nint handle, bool isManuallyAllocated) => new $NAME$Impl(handle, isManuallyAllocated);
-
-$FIELDS$
-}
-"""
-
-ENUM_TEMPLATE = """
-namespace SwiftlyS2.Shared.ProtobufDefinitions;
-
-public enum $NAME$
-{
-$FIELDS$
-}
-"""
-
-all_enum_names = []
-
-def format_template(template: str, params: dict):
- for key, value in params.items():
- template = template.replace(key, value)
- return template
-
-def to_camel_case(name: str):
- return "".join(word.capitalize() for word in name.split("_"))
-
-def get_field_template(field: ast.Field):
-
- global all_enum_names
-
- field.type = field.type.replace(".", "_")
- if field.type.startswith("_"):
- field.type = "." + field.type[1:]
-
- params = {
- "$CSNAME$": to_camel_case(field.name),
- "$NAME$": field.name,
- }
-
- if field.type.removeprefix(".") in all_enum_names:
- params["$CSTYPE$"] = field.type.removeprefix(".")
- if field.cardinality == ast.FieldCardinality.REPEATED:
- print("WTF")
- print(field)
- exit(0)
-
- return format_template(ENUM_FIELD_TEMPLATE, params), format_template(INTERFACE_FIELD_TEMPLATE, params)
-
- if field.type in BASE_TYPES:
-
- params["$TYPE$"] = BASE_TYPES[field.type][0]
- params["$CSTYPE$"] = BASE_TYPES[field.type][1]
- if field.cardinality == ast.FieldCardinality.REPEATED:
- params["$CSTYPE_IMPL$"] = "ProtobufRepeatedFieldValueType<" + params["$CSTYPE$"] + ">"
- params["$CSTYPE$"] = "I" + params["$CSTYPE_IMPL$"]
- return format_template(REPEATED_FIELD_TEMPLATE, params), format_template(INTERFACE_READONLY_FIELD_TEMPLATE, params)
-
- return format_template(FIELD_TEMPLATE, params), format_template(INTERFACE_FIELD_TEMPLATE, params)
-
- if field.type.removeprefix(".") in MANAGED_NESTED_TYPES:
- type = field.type.removeprefix(".")
- params["$TYPE$"] = MANAGED_NESTED_TYPES[type]
- params["$CSTYPE$"] = MANAGED_NESTED_TYPES[type]
-
- if field.cardinality == ast.FieldCardinality.REPEATED:
- params["$CSTYPE_IMPL$"] = "ProtobufRepeatedFieldValueType<" + params["$CSTYPE$"] + ">"
- params["$CSTYPE$"] = "I" + params["$CSTYPE_IMPL$"]
- return format_template(REPEATED_FIELD_TEMPLATE, params), format_template(INTERFACE_READONLY_FIELD_TEMPLATE, params)
-
- return format_template(FIELD_TEMPLATE, params), format_template(INTERFACE_FIELD_TEMPLATE, params)
-
- params["$TYPE$"] = field.type.removeprefix(".")
- params["$CSTYPE$"] = field.type.removeprefix(".")
-
- if field.cardinality == ast.FieldCardinality.REPEATED:
- params["$CSTYPE_IMPL$"] = "ProtobufRepeatedFieldSubMessageType<" + params["$CSTYPE$"] + ">"
- params["$CSTYPE$"] = "I" + params["$CSTYPE_IMPL$"]
- return format_template(REPEATED_FIELD_TEMPLATE, params), format_template(INTERFACE_READONLY_FIELD_TEMPLATE, params)
-
- return format_template(NESTED_FIELD_TEMPLATE, params), format_template(INTERFACE_READONLY_FIELD_TEMPLATE, params)
-
-def write_netmessage(proto_message: ast.Message, net_message_id: int = -1, prefix=""):
- fields = []
- interface_fields = []
-
- for field in proto_message.elements:
-
- if isinstance(field, ast.Field):
- impl_field_template, interface_field_template = get_field_template(field)
- fields.append(impl_field_template)
- interface_fields.append(interface_field_template)
-
- elif isinstance(field, ast.Message):
- write_netmessage(field, -1, prefix + proto_message.name + "_")
-
- params = {
- "$NAME$": prefix + proto_message.name + "Impl",
- "$BASE_CLASS$": "NetMessage" if net_message_id != -1 else "TypedProtobuf",
- "$INTERFACE$": prefix + proto_message.name,
- "$FIELDS$": "\n".join(fields),
- }
- with open(OUT_CLASSES / (prefix+proto_message.name+"Impl.cs"), "w") as f:
- if net_message_id != -1:
- f.write(format_template(NETMESSAGE_IMPL_CLASS_TEMPLATE, params))
- else:
- f.write(format_template(IMPL_CLASS_TEMPLATE, params))
-
- interface_params = {
- "$NAME$": prefix + proto_message.name,
- "$MESSAGE_ID$": str(net_message_id),
- "$MESSAGE_NAME$": prefix + proto_message.name,
- "$FIELDS$": "\n".join(interface_fields),
- }
- if net_message_id != -1:
- with open(OUT_INTERFACES / (prefix+proto_message.name+".cs"), "w") as f:
- f.write(format_template(NETMESSAGE_INTERFACE_TEMPLATE, interface_params))
- else:
- with open(OUT_INTERFACES / (prefix+proto_message.name+".cs"), "w") as f:
- f.write(format_template(NONNETMESSAGE_INTERFACE_TEMPLATE, interface_params))
-
-def write_enum(enum: ast.Enum, prefix=""):
- enum_params = {
- "$NAME$": prefix + enum.name,
- "$FIELDS$": "\n".join([f" {field.name} = {field.number}," if isinstance(field, ast.EnumValue) else "" for field in enum.elements]),
- }
- with open(OUT_ENUMS / (prefix + enum.name+".cs"), "w") as f:
- f.write(format_template(ENUM_TEMPLATE, enum_params))
-
-
-
-def process_enums(root, prefix=""):
- if isinstance(root, ast.File):
- for element in root.file_elements:
- process_enums(element)
- elif isinstance(root, ast.Message):
- for element in root.elements:
- process_enums(element, prefix + root.name + "_")
- elif isinstance(root, ast.Enum):
- all_enum_names.append(prefix + root.name)
- write_enum(root, prefix)
-
-def read_protos():
- global all_enum_names
-
- parser = Parser()
-
- for file in os.listdir(INPUT_DIR):
- with open(os.path.join(INPUT_DIR, file), "r") as f:
- result = parser.parse(f.read())
- process_enums(result)
-
- all_enum_names = [enum.replace(".", "_") for enum in all_enum_names]
-
- for file in os.listdir(INPUT_DIR):
- if True:
- with open(os.path.join(INPUT_DIR, file), "r") as f:
- result = parser.parse(f.read())
-
- messages: List[ast.Message] = []
- handled_messages = []
- enums: List[ast.Enum] = []
- for element in result.file_elements:
- if isinstance(element, ast.Message):
- messages.append(element)
- if isinstance(element, ast.Enum):
- enums.append(element)
-
- for enum in enums:
- if enum.name not in NETMESSAGE_ENUMS:
- continue
- handled_enum_fields = []
- enum_prefix, message_prefix = NETMESSAGE_ENUMS[enum.name]
- # print(enum.elements)
- for enum_field in enum.elements:
- name = enum_field.name.removeprefix(enum_prefix)
- if enum.name == "ETEProtobufIds" or enum.name == "ECsgoGameEvents":
- name = name.removesuffix("Id")
- for message in messages:
- if message_prefix in message.name and name in message.name:
- print(f"{message.name} = {enum_field.number};")
- handled_enum_fields.append(enum_field.name)
- handled_messages.append(message)
- write_netmessage(message, enum_field.number)
-
- for enum_field in enum.elements:
- if enum_field.name not in handled_enum_fields:
- print(f"WARNING: MISSING {enum.name}.{enum_field.name}")
-
- for message in messages:
- if message in handled_messages:
- continue
-
- write_netmessage(message, -1)
-
-read_protos()
\ No newline at end of file
diff --git a/generator/schema_generator/sdk.json b/generator/schema_generator/sdk.json
index 768c85f74..d5aa2055e 100644
--- a/generator/schema_generator/sdk.json
+++ b/generator/schema_generator/sdk.json
@@ -5382,7 +5382,7 @@
"name": "m_numRoundsSurvivedStreak",
"name_hash": 2507309915761382946,
"networked": false,
- "offset": 184,
+ "offset": 240,
"size": 4,
"type": "int32"
},
@@ -5392,7 +5392,7 @@
"name": "m_maxNumRoundsSurvivedStreak",
"name_hash": 2507309914512019198,
"networked": false,
- "offset": 188,
+ "offset": 244,
"size": 4,
"type": "int32"
},
@@ -5402,7 +5402,7 @@
"name": "m_numRoundsSurvivedTotal",
"name_hash": 2507309914753731498,
"networked": false,
- "offset": 192,
+ "offset": 248,
"size": 4,
"type": "int32"
},
@@ -5412,7 +5412,7 @@
"name": "m_iRoundsWonWithoutPurchase",
"name_hash": 2507309916394354374,
"networked": false,
- "offset": 196,
+ "offset": 252,
"size": 4,
"type": "int32"
},
@@ -5422,7 +5422,7 @@
"name": "m_iRoundsWonWithoutPurchaseTotal",
"name_hash": 2507309914486592456,
"networked": false,
- "offset": 200,
+ "offset": 256,
"size": 4,
"type": "int32"
},
@@ -5432,7 +5432,7 @@
"name": "m_numFirstKills",
"name_hash": 2507309914842687180,
"networked": false,
- "offset": 204,
+ "offset": 260,
"size": 4,
"type": "int32"
},
@@ -5442,7 +5442,7 @@
"name": "m_numClutchKills",
"name_hash": 2507309915771339923,
"networked": false,
- "offset": 208,
+ "offset": 264,
"size": 4,
"type": "int32"
},
@@ -5452,7 +5452,7 @@
"name": "m_numPistolKills",
"name_hash": 2507309913627955389,
"networked": false,
- "offset": 212,
+ "offset": 268,
"size": 4,
"type": "int32"
},
@@ -5462,7 +5462,7 @@
"name": "m_numSniperKills",
"name_hash": 2507309917709684257,
"networked": false,
- "offset": 216,
+ "offset": 272,
"size": 4,
"type": "int32"
},
@@ -5472,7 +5472,7 @@
"name": "m_iNumSuicides",
"name_hash": 2507309914674123961,
"networked": false,
- "offset": 220,
+ "offset": 276,
"size": 4,
"type": "int32"
},
@@ -5482,7 +5482,7 @@
"name": "m_iNumTeamKills",
"name_hash": 2507309913554439598,
"networked": false,
- "offset": 224,
+ "offset": 280,
"size": 4,
"type": "int32"
},
@@ -5492,7 +5492,7 @@
"name": "m_flTeamDamage",
"name_hash": 2507309914306972699,
"networked": false,
- "offset": 228,
+ "offset": 284,
"size": 4,
"type": "float32"
}
@@ -5503,7 +5503,7 @@
"name": "CSAdditionalMatchStats_t",
"name_hash": 583778581,
"project": "server",
- "size": 232
+ "size": 288
},
{
"alignment": 8,
@@ -7220,7 +7220,7 @@
"name": "CSAdditionalPerRoundStats_t",
"name_hash": 1906073977,
"project": "server",
- "size": 184
+ "size": 240
},
{
"alignment": 4,
@@ -109182,9 +109182,9 @@
"offset": 3920,
"size": 24,
"template": [
- "SpawnPoint"
+ "CHandle< SpawnPoint >"
],
- "templated": "CUtlVector< SpawnPoint* >",
+ "templated": "CUtlVector< CHandle< SpawnPoint > >",
"type": "CUtlVector"
},
{
@@ -109196,9 +109196,9 @@
"offset": 3944,
"size": 24,
"template": [
- "SpawnPoint"
+ "CHandle< SpawnPoint >"
],
- "templated": "CUtlVector< SpawnPoint* >",
+ "templated": "CUtlVector< CHandle< SpawnPoint > >",
"type": "CUtlVector"
},
{
@@ -109260,9 +109260,9 @@
"offset": 3992,
"size": 24,
"template": [
- "SpawnPoint"
+ "CHandle< SpawnPoint >"
],
- "templated": "CUtlVector< SpawnPoint* >",
+ "templated": "CUtlVector< CHandle< SpawnPoint > >",
"type": "CUtlVector"
},
{
@@ -109274,9 +109274,9 @@
"offset": 4016,
"size": 24,
"template": [
- "SpawnPoint"
+ "CHandle< SpawnPoint >"
],
- "templated": "CUtlVector< SpawnPoint* >",
+ "templated": "CUtlVector< CHandle< SpawnPoint > >",
"type": "CUtlVector"
},
{
@@ -110895,7 +110895,7 @@
"name": "CFireCrackerBlast",
"name_hash": 1729089175,
"project": "server",
- "size": 5056
+ "size": 5048
},
{
"alignment": 255,
@@ -112969,7 +112969,7 @@
"name": "m_firePositions",
"name_hash": 12385185712093863943,
"networked": true,
- "offset": 1848,
+ "offset": 1840,
"size": 768,
"type": "Vector"
},
@@ -112982,7 +112982,7 @@
"name": "m_fireParentPositions",
"name_hash": 12385185714357876183,
"networked": true,
- "offset": 2616,
+ "offset": 2608,
"size": 768,
"type": "Vector"
},
@@ -112995,7 +112995,7 @@
"name": "m_bFireIsBurning",
"name_hash": 12385185715435966572,
"networked": true,
- "offset": 3384,
+ "offset": 3376,
"size": 64,
"type": "bool"
},
@@ -113008,7 +113008,7 @@
"name": "m_BurnNormal",
"name_hash": 12385185712522552283,
"networked": true,
- "offset": 3448,
+ "offset": 3440,
"size": 768,
"type": "Vector"
},
@@ -113018,7 +113018,7 @@
"name": "m_fireCount",
"name_hash": 12385185713762157216,
"networked": true,
- "offset": 4216,
+ "offset": 4208,
"size": 4,
"type": "int32"
},
@@ -113028,7 +113028,7 @@
"name": "m_nInfernoType",
"name_hash": 12385185711643830456,
"networked": true,
- "offset": 4220,
+ "offset": 4212,
"size": 4,
"type": "int32"
},
@@ -113038,7 +113038,7 @@
"name": "m_nFireEffectTickBegin",
"name_hash": 12385185713894414322,
"networked": true,
- "offset": 4224,
+ "offset": 4216,
"size": 4,
"type": "int32"
},
@@ -113048,7 +113048,7 @@
"name": "m_nFireLifetime",
"name_hash": 12385185714581753470,
"networked": true,
- "offset": 4228,
+ "offset": 4220,
"size": 4,
"type": "float32"
},
@@ -113058,7 +113058,7 @@
"name": "m_bInPostEffectTime",
"name_hash": 12385185713256462008,
"networked": true,
- "offset": 4232,
+ "offset": 4224,
"size": 1,
"type": "bool"
},
@@ -113068,7 +113068,7 @@
"name": "m_bWasCreatedInSmoke",
"name_hash": 12385185713136725802,
"networked": false,
- "offset": 4233,
+ "offset": 4225,
"size": 1,
"type": "bool"
},
@@ -113078,7 +113078,7 @@
"name": "m_extent",
"name_hash": 12385185715291201721,
"networked": false,
- "offset": 4752,
+ "offset": 4744,
"size": 24,
"type": "Extent"
},
@@ -113088,7 +113088,7 @@
"name": "m_damageTimer",
"name_hash": 12385185713626568529,
"networked": false,
- "offset": 4776,
+ "offset": 4768,
"size": 24,
"type": "CountdownTimer"
},
@@ -113098,7 +113098,7 @@
"name": "m_damageRampTimer",
"name_hash": 12385185712654275785,
"networked": false,
- "offset": 4800,
+ "offset": 4792,
"size": 24,
"type": "CountdownTimer"
},
@@ -113108,7 +113108,7 @@
"name": "m_splashVelocity",
"name_hash": 12385185713246052213,
"networked": false,
- "offset": 4824,
+ "offset": 4816,
"size": 12,
"templated": "Vector",
"type": "Vector"
@@ -113119,7 +113119,7 @@
"name": "m_InitialSplashVelocity",
"name_hash": 12385185713551459007,
"networked": false,
- "offset": 4836,
+ "offset": 4828,
"size": 12,
"templated": "Vector",
"type": "Vector"
@@ -113130,7 +113130,7 @@
"name": "m_startPos",
"name_hash": 12385185713315889983,
"networked": false,
- "offset": 4848,
+ "offset": 4840,
"size": 12,
"templated": "Vector",
"type": "Vector"
@@ -113141,7 +113141,7 @@
"name": "m_vecOriginalSpawnLocation",
"name_hash": 12385185713163465602,
"networked": false,
- "offset": 4860,
+ "offset": 4852,
"size": 12,
"templated": "Vector",
"type": "Vector"
@@ -113152,7 +113152,7 @@
"name": "m_activeTimer",
"name_hash": 12385185712771665156,
"networked": false,
- "offset": 4872,
+ "offset": 4864,
"size": 16,
"type": "IntervalTimer"
},
@@ -113162,7 +113162,7 @@
"name": "m_fireSpawnOffset",
"name_hash": 12385185711790040719,
"networked": false,
- "offset": 4888,
+ "offset": 4880,
"size": 4,
"type": "int32"
},
@@ -113172,7 +113172,7 @@
"name": "m_nMaxFlames",
"name_hash": 12385185713501527865,
"networked": false,
- "offset": 4892,
+ "offset": 4884,
"size": 4,
"type": "int32"
},
@@ -113182,7 +113182,7 @@
"name": "m_nSpreadCount",
"name_hash": 12385185715648476129,
"networked": false,
- "offset": 4896,
+ "offset": 4888,
"size": 4,
"type": "int32"
},
@@ -113192,7 +113192,7 @@
"name": "m_BookkeepingTimer",
"name_hash": 12385185713543863756,
"networked": false,
- "offset": 4904,
+ "offset": 4896,
"size": 24,
"type": "CountdownTimer"
},
@@ -113202,7 +113202,7 @@
"name": "m_NextSpreadTimer",
"name_hash": 12385185712390350876,
"networked": false,
- "offset": 4928,
+ "offset": 4920,
"size": 24,
"type": "CountdownTimer"
},
@@ -113212,7 +113212,7 @@
"name": "m_nSourceItemDefIndex",
"name_hash": 12385185711675200230,
"networked": false,
- "offset": 4952,
+ "offset": 4944,
"size": 2,
"type": "uint16"
}
@@ -113223,7 +113223,7 @@
"name": "CInferno",
"name_hash": 2883650761,
"project": "server",
- "size": 5056
+ "size": 5048
},
{
"alignment": 16,
@@ -117302,7 +117302,7 @@
"name": "CCSPlayerController_ActionTrackingServices",
"name_hash": 2531222465,
"project": "server",
- "size": 944
+ "size": 1056
},
{
"alignment": 8,
@@ -125346,25 +125346,15 @@
"offset": 3024,
"size": 16,
"type": "IntervalTimer"
- },
- {
- "alignment": 1,
- "kind": "ref",
- "name": "m_bHasBouncedOffPlayer",
- "name_hash": 11689632208429145979,
- "networked": false,
- "offset": 3248,
- "size": 1,
- "type": "bool"
}
],
- "fields_count": 4,
+ "fields_count": 3,
"has_chainer": false,
"is_struct": false,
"name": "CMolotovProjectile",
"name_hash": 2721704591,
"project": "server",
- "size": 3264
+ "size": 3248
},
{
"alignment": 16,
@@ -136594,7 +136584,7 @@
"name_hash": 15135923261766619426,
"networked": true,
"offset": 1376,
- "size": 48,
+ "size": 56,
"type": "CCSPlayerModernJump"
},
{
@@ -136603,7 +136593,7 @@
"name": "m_nLastJumpTick",
"name_hash": 15135923263336935020,
"networked": true,
- "offset": 1424,
+ "offset": 1432,
"size": 4,
"type": "GameTick_t"
},
@@ -136613,7 +136603,7 @@
"name": "m_flLastJumpFrac",
"name_hash": 15135923261856551051,
"networked": true,
- "offset": 1428,
+ "offset": 1436,
"size": 4,
"type": "float32"
},
@@ -136623,7 +136613,7 @@
"name": "m_flLastJumpVelocityZ",
"name_hash": 15135923260727144450,
"networked": true,
- "offset": 1432,
+ "offset": 1440,
"size": 4,
"type": "float32"
},
@@ -136633,7 +136623,7 @@
"name": "m_bJumpApexPending",
"name_hash": 15135923263330754384,
"networked": true,
- "offset": 1436,
+ "offset": 1444,
"size": 1,
"type": "bool"
},
@@ -136643,7 +136633,7 @@
"name": "m_flTicksSinceLastSurfingDetected",
"name_hash": 15135923261186133279,
"networked": false,
- "offset": 1440,
+ "offset": 1448,
"size": 4,
"type": "float32"
},
@@ -136653,7 +136643,7 @@
"name": "m_bWasSurfing",
"name_hash": 15135923263609373166,
"networked": true,
- "offset": 1444,
+ "offset": 1452,
"size": 1,
"type": "bool"
},
@@ -136663,7 +136653,7 @@
"name": "m_vecInputRotated",
"name_hash": 15135923262573175124,
"networked": false,
- "offset": 1588,
+ "offset": 1596,
"size": 12,
"templated": "Vector",
"type": "Vector"
@@ -136675,7 +136665,7 @@
"name": "CCSPlayer_MovementServices",
"name_hash": 3524106755,
"project": "server",
- "size": 3680
+ "size": 3688
},
{
"alignment": 16,
@@ -142288,15 +142278,25 @@
"offset": 44,
"size": 4,
"type": "float32"
+ },
+ {
+ "alignment": 4,
+ "kind": "ref",
+ "name": "m_flLastLandedVelocityZ",
+ "name_hash": 10149089920638860986,
+ "networked": true,
+ "offset": 48,
+ "size": 4,
+ "type": "float32"
}
],
- "fields_count": 8,
+ "fields_count": 9,
"has_chainer": false,
"is_struct": true,
"name": "CCSPlayerModernJump",
"name_hash": 2363019138,
"project": "server",
- "size": 48
+ "size": 56
},
{
"alignment": 8,
diff --git a/managed/managed.csproj b/managed/managed.csproj
index 2fd66e00f..957cd60bf 100644
--- a/managed/managed.csproj
+++ b/managed/managed.csproj
@@ -53,6 +53,7 @@
+
diff --git a/managed/src/SwiftlyS2.Core/AttributeParsers/CommandAttributeParser.cs b/managed/src/SwiftlyS2.Core/AttributeParsers/CommandAttributeParser.cs
index 2d7c7bc56..60a74c17f 100644
--- a/managed/src/SwiftlyS2.Core/AttributeParsers/CommandAttributeParser.cs
+++ b/managed/src/SwiftlyS2.Core/AttributeParsers/CommandAttributeParser.cs
@@ -20,8 +20,9 @@ public static void ParseFromObject( this ICommandService self, object instance )
var commandName = commandAttribute.Name;
var registerRaw = commandAttribute.RegisterRaw;
var permission = commandAttribute.Permission;
+ var helpText = commandAttribute.HelpText;
- var cmdGuid = self.RegisterCommand(commandName, method.CreateDelegate(instance), registerRaw, permission);
+ var cmdGuid = self.RegisterCommand(commandName, method.CreateDelegate(instance), registerRaw, permission, helpText);
foreach (var aliasAttr in commandAliasAttributes)
{
self.RegisterCommandAlias(commandName, aliasAttr.Alias, aliasAttr.RegisterRaw);
diff --git a/managed/src/SwiftlyS2.Core/Misc/SwiftlyLogger.cs b/managed/src/SwiftlyS2.Core/Misc/SwiftlyLogger.cs
index 5e5b98b6b..63e7911c2 100644
--- a/managed/src/SwiftlyS2.Core/Misc/SwiftlyLogger.cs
+++ b/managed/src/SwiftlyS2.Core/Misc/SwiftlyLogger.cs
@@ -20,6 +20,7 @@ internal class SwiftlyLogger( string categoryName, string contextName ) : ILogge
[LogLevel.Error] = ("Error", "red3"),
[LogLevel.Critical] = ("Critical", "red3")
};
+ private static readonly bool HideLogInConsole = ShouldHideLogToConsole();
public IDisposable BeginScope( TState state ) where TState : notnull => NullScope.Instance;
@@ -37,15 +38,14 @@ public void Log( LogLevel logLevel, EventId eventId, TState state, Excep
var eventIdText = eventId.Id != 0 ? $"[{eventId.Id}]" : string.Empty;
AnsiConsole.Profile.Width = 13337;
- // Console output
- AnsiConsole.MarkupLineInterpolated($"[lightsteelblue1 bold]{contextName}[/] [lightsteelblue]|[/] [grey42]{timestamp}[/] [lightsteelblue]|[/] [{color}]{levelText}[/] [lightsteelblue]|[/] [lightsteelblue]{categoryName}{eventIdText}[/]");
+ if (!HideLogInConsole || categoryName.Contains("Core") || categoryName.Contains("Shared")) AnsiConsole.MarkupLineInterpolated($"[lightsteelblue1 bold]{contextName}[/] [lightsteelblue]|[/] [grey42]{timestamp}[/] [lightsteelblue]|[/] [{color}]{levelText}[/] [lightsteelblue]|[/] [lightsteelblue]{categoryName}{eventIdText}[/]");
// Message output
var message = formatter?.Invoke(state, exception) ?? state?.ToString();
if (!string.IsNullOrEmpty(message))
{
FileLogger.Log($"{contextName} | {timestamp} | {levelText} | {categoryName}{eventIdText} | {message}");
- OutputMessageLines(message);
+ if (!HideLogInConsole || categoryName.Contains("Core") || categoryName.Contains("Shared")) OutputMessageLines(message);
}
// Exception output
@@ -83,6 +83,12 @@ private static LogLevel GetMinLogLevelFromEnv()
};
}
+ private static bool ShouldHideLogToConsole()
+ {
+ var output = Environment.GetEnvironmentVariable("SWIFTLY_HIDE_LOG_IN_CONSOLE")?.ToUpperInvariant();
+ return output == "1" || output == "TRUE" || output == "YES";
+ }
+
private sealed class NullScope : IDisposable
{
public static readonly NullScope Instance = new();
diff --git a/managed/src/SwiftlyS2.Core/Modules/Commands/CommandCallback.cs b/managed/src/SwiftlyS2.Core/Modules/Commands/CommandCallback.cs
index b72c88447..b457ea537 100644
--- a/managed/src/SwiftlyS2.Core/Modules/Commands/CommandCallback.cs
+++ b/managed/src/SwiftlyS2.Core/Modules/Commands/CommandCallback.cs
@@ -40,7 +40,7 @@ internal class CommandCallback : CommandCallbackBase
private readonly ulong nativeListenerId;
private readonly ILogger logger;
- public CommandCallback( string commandName, bool registerRaw, ICommandService.CommandListener handler, string permission, IPlayerManagerService playerManagerService, IPermissionManager permissionManager, ILoggerFactory loggerFactory, IContextedProfilerService profiler ) : base(loggerFactory, profiler)
+ public CommandCallback( string commandName, bool registerRaw, ICommandService.CommandListener handler, string permission, string helpText, IPlayerManagerService playerManagerService, IPermissionManager permissionManager, ILoggerFactory loggerFactory, IContextedProfilerService profiler ) : base(loggerFactory, profiler)
{
this.logger = LoggerFactory.CreateLogger();
@@ -59,7 +59,7 @@ public CommandCallback( string commandName, bool registerRaw, ICommandService.Co
var commandNameString = Marshal.PtrToStringUTF8(commandNamePtr)!;
var prefixString = Marshal.PtrToStringUTF8(prefixPtr)!;
- var args = argsString.Split('\x01').Where(s => !string.IsNullOrWhiteSpace(s)).ToArray();
+ var args = argsString.Split('\x01').ToArray();
var context = new CommandContext(playerId, args, commandNameString, prefixString, slient == 1);
if (!context.IsSentByPlayer || string.IsNullOrWhiteSpace(commandPermissions) || permissionManager.PlayerHasPermission(playerManagerService.GetPlayer(playerId)?.SteamID ?? 0, commandPermissions))
{
@@ -79,7 +79,7 @@ public CommandCallback( string commandName, bool registerRaw, ICommandService.Co
};
commandCallbackPtr = Marshal.GetFunctionPointerForDelegate(commandCallback);
- nativeListenerId = NativeCommands.RegisterCommand(commandName, commandCallbackPtr, registerRaw);
+ nativeListenerId = NativeCommands.RegisterCommand(commandName, commandCallbackPtr, registerRaw, helpText);
}
public override void Dispose()
diff --git a/managed/src/SwiftlyS2.Core/Modules/Commands/CommandService.cs b/managed/src/SwiftlyS2.Core/Modules/Commands/CommandService.cs
index 71136447d..7050a2600 100644
--- a/managed/src/SwiftlyS2.Core/Modules/Commands/CommandService.cs
+++ b/managed/src/SwiftlyS2.Core/Modules/Commands/CommandService.cs
@@ -32,9 +32,14 @@ public CommandService( ILoggerFactory loggerFactory, IContextedProfilerService p
}
}
- public Guid RegisterCommand( string commandName, ICommandService.CommandListener handler, bool registerRaw = false, string permission = "" )
+ public Guid RegisterCommand( string commandName, ICommandService.CommandListener handler, bool registerRaw, string permission )
{
- var callback = new CommandCallback(commandName, registerRaw, handler, permission, playerManagerService, permissionManager, loggerFactory, profiler);
+ return RegisterCommand(commandName, handler, registerRaw, permission, "SwiftlyS2 registered command");
+ }
+
+ public Guid RegisterCommand( string commandName, ICommandService.CommandListener handler, bool registerRaw = false, string permission = "", string helpText = "SwiftlyS2 registered command" )
+ {
+ var callback = new CommandCallback(commandName, registerRaw, handler, permission, helpText, playerManagerService, permissionManager, loggerFactory, profiler);
lock (commandLock)
{
commandCallbacks.Add(callback);
diff --git a/managed/src/SwiftlyS2.Core/Modules/Datamaps/BaseDatamapFunction.cs b/managed/src/SwiftlyS2.Core/Modules/Datamaps/BaseDatamapFunction.cs
index 85dfa968a..8d5f9e920 100644
--- a/managed/src/SwiftlyS2.Core/Modules/Datamaps/BaseDatamapFunction.cs
+++ b/managed/src/SwiftlyS2.Core/Modules/Datamaps/BaseDatamapFunction.cs
@@ -1,9 +1,7 @@
using System.Runtime.InteropServices;
using Spectre.Console;
using SwiftlyS2.Core.Extensions;
-using SwiftlyS2.Core.Hooks;
using SwiftlyS2.Core.Natives;
-using SwiftlyS2.Shared;
using SwiftlyS2.Shared.Datamaps;
using SwiftlyS2.Shared.Profiler;
using SwiftlyS2.Shared.Schemas;
diff --git a/managed/src/SwiftlyS2.Core/Modules/Engine/TraceManager.cs b/managed/src/SwiftlyS2.Core/Modules/Engine/TraceManager.cs
index 7f66d9b26..6257a9c93 100644
--- a/managed/src/SwiftlyS2.Core/Modules/Engine/TraceManager.cs
+++ b/managed/src/SwiftlyS2.Core/Modules/Engine/TraceManager.cs
@@ -14,6 +14,7 @@ public void TracePlayerBBox( Vector start, Vector end, BBox_t bounds, CTraceFilt
{
fixed (CGameTrace* tracePtr = &trace)
{
+ filter.EnsureValid();
GameFunctions.TracePlayerBBox(start, end, bounds, &filter, tracePtr);
}
}
diff --git a/managed/src/SwiftlyS2.Core/Modules/EntitySystem/ClassConvertor.cs b/managed/src/SwiftlyS2.Core/Modules/EntitySystem/ClassConvertor.cs
new file mode 100644
index 000000000..dbb5db6e4
--- /dev/null
+++ b/managed/src/SwiftlyS2.Core/Modules/EntitySystem/ClassConvertor.cs
@@ -0,0 +1,459 @@
+using SwiftlyS2.Shared.SchemaDefinitions;
+
+namespace SwiftlyS2.Core.SchemaDefinitions;
+
+internal static class ClassConvertor
+{
+ public static CEntityInstance ConvertEntityByDesignerName( nint address, string designerName )
+ {
+ return designerName switch {
+ "cs_player_controller" => new CCSPlayerControllerImpl(address),
+ "player" => new CCSPlayerPawnImpl(address),
+ "observer" => new CCSObserverPawnImpl(address),
+ "spawnpoint" => new SpawnPointImpl(address),
+ "filter_health" => new FilterHealthImpl(address),
+ "filter_damage_type" => new FilterDamageTypeImpl(address),
+ "worldent" => new CWorldImpl(address),
+ "weapon_xm1014" => new CWeaponXM1014Impl(address),
+ "weapon_usp_silencer" => new CWeaponUSPSilencerImpl(address),
+ "weapon_ump45" => new CWeaponUMP45Impl(address),
+ "weapon_tec9" => new CWeaponTec9Impl(address),
+ "weapon_taser" => new CWeaponTaserImpl(address),
+ "weapon_sawedoff" => new CWeaponSawedoffImpl(address),
+ "weapon_ssg08" => new CWeaponSSG08Impl(address),
+ "weapon_sg556" => new CWeaponSG556Impl(address),
+ "weapon_scar20" => new CWeaponSCAR20Impl(address),
+ "weapon_revolver" => new CWeaponRevolverImpl(address),
+ "weapon_p90" => new CWeaponP90Impl(address),
+ "weapon_p250" => new CWeaponP250Impl(address),
+ "weapon_negev" => new CWeaponNegevImpl(address),
+ "weapon_nova" => new CWeaponNOVAImpl(address),
+ "weapon_mag7" => new CWeaponMag7Impl(address),
+ "weapon_mp9" => new CWeaponMP9Impl(address),
+ "weapon_mp7" => new CWeaponMP7Impl(address),
+ "weapon_mp5sd" => new CWeaponMP5SDImpl(address),
+ "weapon_mac10" => new CWeaponMAC10Impl(address),
+ "weapon_m4a1_silencer" => new CWeaponM4A1SilencerImpl(address),
+ "weapon_m4a1" => new CWeaponM4A1Impl(address),
+ "weapon_m249" => new CWeaponM249Impl(address),
+ "weapon_hkp2000" => new CWeaponHKP2000Impl(address),
+ "weapon_glock" => new CWeaponGlockImpl(address),
+ "weapon_galilar" => new CWeaponGalilARImpl(address),
+ "weapon_g3sg1" => new CWeaponG3SG1Impl(address),
+ "weapon_fiveseven" => new CWeaponFiveSevenImpl(address),
+ "weapon_famas" => new CWeaponFamasImpl(address),
+ "weapon_elite" => new CWeaponEliteImpl(address),
+ "weapon_cz75a" => new CWeaponCZ75aImpl(address),
+ "weapon_bizon" => new CWeaponBizonImpl(address),
+ "weapon_csbase" => new CWeaponBaseItemImpl(address),
+ "weapon_aug" => new CWeaponAugImpl(address),
+ "weapon_awp" => new CWeaponAWPImpl(address),
+ "waterbullet" => new CWaterBulletImpl(address),
+ "vote_controller" => new CVoteControllerImpl(address),
+ "trigger_transition" => new CTriggerVolumeImpl(address),
+ "trigger_togglesave" => new CTriggerToggleSaveImpl(address),
+ "trigger_teleport" => new CTriggerTeleportImpl(address),
+ "trigger_soundscape" => new CTriggerSoundscapeImpl(address),
+ "trigger_snd_sos_opvar" => new CTriggerSndSosOpvarImpl(address),
+ "trigger_autosave" => new CTriggerSaveImpl(address),
+ "trigger_remove" => new CTriggerRemoveImpl(address),
+ "trigger_push" => new CTriggerPushImpl(address),
+ "trigger_proximity" => new CTriggerProximityImpl(address),
+ "trigger_physics" => new CTriggerPhysicsImpl(address),
+ "trigger_once" => new CTriggerOnceImpl(address),
+ "trigger_multiple" => new CTriggerMultipleImpl(address),
+ "trigger_look" => new CTriggerLookImpl(address),
+ "trigger_lerp_object" => new CTriggerLerpObjectImpl(address),
+ "trigger_impact" => new CTriggerImpactImpl(address),
+ "trigger_hurt" => new CTriggerHurtImpl(address),
+ "trigger_hostage_reset" => new CTriggerHostageResetImpl(address),
+ "trigger_gravity" => new CTriggerGravityImpl(address),
+ "trigger_game_event" => new CTriggerGameEventImpl(address),
+ "trigger_fan" => new CTriggerFanImpl(address),
+ "trigger_detect_explosion" => new CTriggerDetectExplosionImpl(address),
+ "trigger_detect_bullet_fire" => new CTriggerDetectBulletFireImpl(address),
+ "trigger_callback" => new CTriggerCallbackImpl(address),
+ "trigger_buoyancy" => new CTriggerBuoyancyImpl(address),
+ "trigger_brush" => new CTriggerBrushImpl(address),
+ "trigger_bomb_reset" => new CTriggerBombResetImpl(address),
+ "trigger_active_weapon_detect" => new CTriggerActiveWeaponDetectImpl(address),
+ "trigger_tonemap" => new CTonemapTriggerImpl(address),
+ "env_tonemap_controller2" => new CTonemapController2Impl(address),
+ "logic_timer" => new CTimerEntityImpl(address),
+ "hl_vr_texture_based_animatable" => new CTextureBasedAnimatableImpl(address),
+ "test_io_combinations" => new CTestPulseIOImpl(address),
+ "test_effect" => new CTestEffectImpl(address),
+ "team_manager" => new CTeamImpl(address),
+ "tanktrain_ai" => new CTankTrainAIImpl(address),
+ "tanktrain_aitarget" => new CTankTargetChangeImpl(address),
+ "env_sprite_oriented" => new CSpriteOrientedImpl(address),
+ "env_glow" => new CSpriteImpl(address),
+ "spotlight_end" => new CSpotlightEndImpl(address),
+ "phys_splineconstraint" => new CSplineConstraintImpl(address),
+ "snd_stack_save" => new CSoundStackSaveImpl(address),
+ "snd_opvar_set_point" => new CSoundOpvarSetPointEntityImpl(address),
+ "snd_opvar_set_point_base" => new CSoundOpvarSetPointBaseImpl(address),
+ "snd_opvar_set_path_corner" => new CSoundOpvarSetPathCornerEntityImpl(address),
+ "snd_opvar_set_wind_obb" => new CSoundOpvarSetOBBWindEntityImpl(address),
+ "snd_opvar_set_obb" => new CSoundOpvarSetOBBEntityImpl(address),
+ "snd_opvar_set" => new CSoundOpvarSetEntityImpl(address),
+ "snd_opvar_set_auto_room" => new CSoundOpvarSetAutoRoomEntityImpl(address),
+ "snd_opvar_set_aabb" => new CSoundOpvarSetAABBEntityImpl(address),
+ "snd_event_cone" => new CSoundEventConeEntityImpl(address),
+ "snd_event_sphere" => new CSoundEventSphereEntityImpl(address),
+ "snd_event_path_corner" => new CSoundEventPathCornerEntityImpl(address),
+ "snd_event_param" => new CSoundEventParameterImpl(address),
+ "snd_event_orientedbox" => new CSoundEventOBBEntityImpl(address),
+ "snd_event_point" => new CSoundEventEntityImpl(address),
+ "snd_event_alignedbox" => new CSoundEventAABBEntityImpl(address),
+ "snd_sound_area_sphere" => new CSoundAreaEntitySphereImpl(address),
+ "snd_sound_area_obb" => new CSoundAreaEntityOrientedBoxImpl(address),
+ "snd_sound_area_base" => new CSoundAreaEntityBaseImpl(address),
+ "smokegrenade_projectile" => new CSmokeGrenadeProjectileImpl(address),
+ "weapon_smokegrenade" => new CSmokeGrenadeImpl(address),
+ "skybox_reference" => new CSkyboxReferenceImpl(address),
+ "sky_camera" => new CSkyCameraImpl(address),
+ "markup_volume_tagged" => new CSimpleMarkupVolumeTaggedImpl(address),
+ "spark_shower" => new CShowerImpl(address),
+ "shatterglass_shard" => new CShatterGlassShardPhysicsImpl(address),
+ "trigger_serverragdoll" => new CServerRagdollTriggerImpl(address),
+ "scripted_sequence" => new CScriptedSequenceImpl(address),
+ "script_trigger_push" => new CScriptTriggerPushImpl(address),
+ "script_trigger_once" => new CScriptTriggerOnceImpl(address),
+ "script_trigger_multiple" => new CScriptTriggerMultipleImpl(address),
+ "script_trigger_hurt" => new CScriptTriggerHurtImpl(address),
+ "script_nav_blocker" => new CScriptNavBlockerImpl(address),
+ "scripted_item_drop" => new CScriptItemImpl(address),
+ "logic_scene_list_manager" => new CSceneListManagerImpl(address),
+ "scripted_scene" => new CSceneEntityImpl(address),
+ "rotator_target" => new CRotatorTargetImpl(address),
+ "func_door_rotating" => new CRotDoorImpl(address),
+ "func_rot_button" => new CRotButtonImpl(address),
+ "keyframe_rope" => new CRopeKeyframeImpl(address),
+ "player_loadsaved" => new CRevertSavedImpl(address),
+ "light_rect" => new CRectLightImpl(address),
+ "prop_ragdoll_attached" => new CRagdollPropAttachedImpl(address),
+ "prop_ragdoll" => new CRagdollPropImpl(address),
+ "game_ragdoll_manager" => new CRagdollManagerImpl(address),
+ "phys_ragdollmagnet" => new CRagdollMagnetImpl(address),
+ "phys_ragdollconstraint" => new CRagdollConstraintImpl(address),
+ "func_pushable" => new CPushableImpl(address),
+ "pulse_game_blackboard" => new CPulseGameBlackboardImpl(address),
+ "prop_door_rotating" => new CPropDoorRotatingBreakableImpl(address),
+ "func_precipitation_blocker" => new CPrecipitationBlockerImpl(address),
+ "func_precipitation" => new CPrecipitationImpl(address),
+ "post_processing_volume" => new CPostProcessingVolumeImpl(address),
+ "point_script" => new CCSPointScriptEntityImpl(address),
+ "point_worldtext" => new CPointWorldTextImpl(address),
+ "point_velocitysensor" => new CPointVelocitySensorImpl(address),
+ "point_value_remapper" => new CPointValueRemapperImpl(address),
+ "point_template" => new CPointTemplateImpl(address),
+ "point_teleport" => new CPointTeleportImpl(address),
+ "point_servercommand" => new CPointServerCommandImpl(address),
+ "point_push" => new CPointPushImpl(address),
+ "point_pulse" => new CPointPulseImpl(address),
+ "point_proximity_sensor" => new CPointProximitySensorImpl(address),
+ "point_prefab" => new CPointPrefabImpl(address),
+ "point_orient" => new CPointOrientImpl(address),
+ "point_hurt" => new CPointHurtImpl(address),
+ "point_give_ammo" => new CPointGiveAmmoImpl(address),
+ "point_gamestats_counter" => new CPointGamestatsCounterImpl(address),
+ "point_entity_finder" => new CPointEntityFinderImpl(address),
+ "point_entity" => new CPointEntityImpl(address),
+ "point_commentary_node" => new CPointCommentaryNodeImpl(address),
+ "point_clientui_world_text_panel" => new CPointClientUIWorldTextPanelImpl(address),
+ "point_clientui_world_panel" => new CPointClientUIWorldPanelImpl(address),
+ "point_clientui_dialog" => new CPointClientUIDialogImpl(address),
+ "point_clientcommand" => new CPointClientCommandImpl(address),
+ "point_childmodifier" => new CPointChildModifierImpl(address),
+ "point_camera_vertical_fov" => new CPointCameraVFOVImpl(address),
+ "point_camera" => new CPointCameraImpl(address),
+ "point_broadcastclientcommand" => new CPointBroadcastClientCommandImpl(address),
+ "point_angularvelocitysensor" => new CPointAngularVelocitySensorImpl(address),
+ "point_anglesensor" => new CPointAngleSensorImpl(address),
+ "env_player_visibility" => new CPlayerVisibilityImpl(address),
+ "player_spray_decal" => new CPlayerSprayDecalImpl(address),
+ "info_player_ping" => new CPlayerPingImpl(address),
+ "plat_trigger" => new CPlatTriggerImpl(address),
+ "planted_c4" => new CPlantedC4Impl(address),
+ "env_physwire" => new CPhysicsWireImpl(address),
+ "phys_spring" => new CPhysicsSpringImpl(address),
+ "prop_physics_respawnable" => new CPhysicsPropRespawnableImpl(address),
+ "prop_physics_override" => new CPhysicsPropOverrideImpl(address),
+ "prop_physics_multiplayer" => new CPhysicsPropMultiplayerImpl(address),
+ "prop_physics" => new CPhysicsPropImpl(address),
+ "physics_entity_solver" => new CPhysicsEntitySolverImpl(address),
+ "func_physical_button" => new CPhysicalButtonImpl(address),
+ "phys_wheelconstraint" => new CPhysWheelConstraintImpl(address),
+ "phys_torque" => new CPhysTorqueImpl(address),
+ "phys_thruster" => new CPhysThrusterImpl(address),
+ "phys_slideconstraint" => new CPhysSlideConstraintImpl(address),
+ "phys_pulleyconstraint" => new CPhysPulleyImpl(address),
+ "phys_motor" => new CPhysMotorImpl(address),
+ "phys_magnet" => new CPhysMagnetImpl(address),
+ "phys_lengthconstraint" => new CPhysLengthImpl(address),
+ "env_physimpact" => new CPhysImpactImpl(address),
+ "phys_hinge" => new CPhysHingeImpl(address),
+ "phys_constraint" => new CPhysFixedImpl(address),
+ "env_physexplosion" => new CPhysExplosionImpl(address),
+ "func_physbox" => new CPhysBoxImpl(address),
+ "phys_ballsocket" => new CPhysBallSocketImpl(address),
+ "path_track" => new CPathTrackImpl(address),
+ "path_simple" => new CPathSimpleImpl(address),
+ "path_particle_rope" => new CPathParticleRopeImpl(address),
+ "path_mover" => new CPathMoverImpl(address),
+ "keyframe_track" => new CPathKeyFrameImpl(address),
+ "path_corner_crash" => new CPathCornerCrashImpl(address),
+ "path_corner" => new CPathCornerImpl(address),
+ "info_particle_system" => new CParticleSystemImpl(address),
+ "prop_dynamic_ornament" => new COrnamentPropImpl(address),
+ "light_omni2" => new COmniLightImpl(address),
+ "info_null" => new CNullEntityImpl(address),
+ "point_nav_walkable" => new CNavWalkableImpl(address),
+ "info_nav_space" => new CNavSpaceInfoImpl(address),
+ "ai_nav_link_area" => new CNavLinkAreaEntityImpl(address),
+ "multisource" => new CMultiSourceImpl(address),
+ "logic_multilight_proxy" => new CMultiLightProxyImpl(address),
+ "path_node_mover" => new CMoverPathNodeImpl(address),
+ "momentary_rot_button" => new CMomentaryRotButtonImpl(address),
+ "molotov_projectile" => new CMolotovProjectileImpl(address),
+ "weapon_molotov" => new CMolotovGrenadeImpl(address),
+ "point_message" => new CMessageEntityImpl(address),
+ "env_message" => new CMessageImpl(address),
+ "math_remap" => new CMathRemapImpl(address),
+ "math_counter" => new CMathCounterImpl(address),
+ "math_colorblend" => new CMathColorBlendImpl(address),
+ "markup_volume_with_ref" => new CMarkupVolumeWithRefImpl(address),
+ "func_nav_markup_game" => new CMarkupVolumeTagged_NavGameImpl(address),
+ "func_nav_markup" => new CMarkupVolumeTagged_NavImpl(address),
+ "markup_volume" => new CMarkupVolumeImpl(address),
+ "mapvetopick_controller" => new CMapVetoPickControllerImpl(address),
+ "map_shared_environment" => new CMapSharedEnvironmentImpl(address),
+ "info_map_parameters" => new CMapInfoImpl(address),
+ "logic_script" => new CLogicScriptImpl(address),
+ "logic_relay" => new CLogicRelayImpl(address),
+ "logic_proximity" => new CLogicProximityImpl(address),
+ "logic_playerproxy" => new CLogicPlayerProxyImpl(address),
+ "logic_navigation" => new CLogicNavigationImpl(address),
+ "logic_npc_counter_obb" => new CLogicNPCCounterOBBImpl(address),
+ "logic_npc_counter_aabb" => new CLogicNPCCounterAABBImpl(address),
+ "logic_npc_counter_radius" => new CLogicNPCCounterImpl(address),
+ "logic_measure_movement" => new CLogicMeasureMovementImpl(address),
+ "logic_lineto" => new CLogicLineToEntityImpl(address),
+ "logic_gameevent_listener" => new CLogicGameEventListenerImpl(address),
+ "logic_game_event" => new CLogicGameEventImpl(address),
+ "logic_eventlistener" => new CLogicEventListenerImpl(address),
+ "logic_distance_check" => new CLogicDistanceCheckImpl(address),
+ "logic_distance_autosave" => new CLogicDistanceAutosaveImpl(address),
+ "logic_compare" => new CLogicCompareImpl(address),
+ "logic_collision_pair" => new CLogicCollisionPairImpl(address),
+ "logic_case" => new CLogicCaseImpl(address),
+ "logic_branch_listener" => new CLogicBranchListImpl(address),
+ "logic_branch" => new CLogicBranchImpl(address),
+ "logic_autosave" => new CLogicAutosaveImpl(address),
+ "logic_auto" => new CLogicAutoImpl(address),
+ "logic_active_autosave" => new CLogicActiveAutosaveImpl(address),
+ "logic_achievement" => new CLogicAchievementImpl(address),
+ "light_spot" => new CLightSpotEntityImpl(address),
+ "light_ortho" => new CLightOrthoEntityImpl(address),
+ "light_environment" => new CLightEnvironmentEntityImpl(address),
+ "light_omni" => new CLightEntityImpl(address),
+ "light_directional" => new CLightDirectionalEntityImpl(address),
+ "weapon_knife" => new CKnifeImpl(address),
+ "phys_keepupright" => new CKeepUprightImpl(address),
+ "weapon_healthshot" => new CItem_HealthshotImpl(address),
+ "item_sodacan" => new CItemSodaImpl(address),
+ "item_kevlar" => new CItemKevlarImpl(address),
+ "item_generic_trigger_helper" => new CItemGenericTriggerHelperImpl(address),
+ "item_generic" => new CItemGenericImpl(address),
+ "item_defuser" => new CItemDefuserImpl(address),
+ "item_assaultsuit" => new CItemAssaultSuitImpl(address),
+ "point_instructor_event" => new CInstructorEventEntityImpl(address),
+ "instanced_scripted_scene" => new CInstancedSceneEntityImpl(address),
+ "info_world_layer" => new CInfoWorldLayerImpl(address),
+ "info_visibility_box" => new CInfoVisibilityBoxImpl(address),
+ "info_teleport_destination" => new CInfoTeleportDestinationImpl(address),
+ "info_target_server_only" => new CInfoTargetServerOnlyImpl(address),
+ "info_target" => new CInfoTargetImpl(address),
+ "info_spawngroup_load_unload" => new CInfoSpawnGroupLoadUnloadImpl(address),
+ "info_spawngroup_landmark" => new CInfoSpawnGroupLandmarkImpl(address),
+ "info_player_terrorist" => new CInfoPlayerTerroristImpl(address),
+ "info_player_start" => new CInfoPlayerStartImpl(address),
+ "info_player_counterterrorist" => new CInfoPlayerCounterterroristImpl(address),
+ "info_particle_target" => new CInfoParticleTargetImpl(address),
+ "info_offscreen_panorama_texture" => new CInfoOffscreenPanoramaTextureImpl(address),
+ "info_landmark" => new CInfoLandmarkImpl(address),
+ "info_ladder_dismount" => new CInfoLadderDismountImpl(address),
+ "info_target_instructor_hint" => new CInfoInstructorHintTargetImpl(address),
+ "info_hostage_rescue_zone_hint" => new CInfoInstructorHintHostageRescueZoneImpl(address),
+ "info_bomb_target_hint_B" => new CInfoInstructorHintBombTargetBImpl(address),
+ "info_bomb_target_hint_A" => new CInfoInstructorHintBombTargetAImpl(address),
+ "info_game_event_proxy" => new CInfoGameEventProxyImpl(address),
+ "info_trigger_fan" => new CInfoFanImpl(address),
+ "info_dynamic_shadow_hint_box" => new CInfoDynamicShadowHintBoxImpl(address),
+ "info_dynamic_shadow_hint" => new CInfoDynamicShadowHintImpl(address),
+ "info_deathmatch_spawn" => new CInfoDeathmatchSpawnImpl(address),
+ "info_data" => new CInfoDataImpl(address),
+ "inferno" => new CInfernoImpl(address),
+ "weapon_incgrenade" => new CIncendiaryGrenadeImpl(address),
+ "func_hostage_rescue" => new CHostageRescueZoneImpl(address),
+ "hostage_entity" => new CHostageImpl(address),
+ "handle_test" => new CHandleTestImpl(address),
+ "handle_dummy" => new CHandleDummyImpl(address),
+ "hegrenade_projectile" => new CHEGrenadeProjectileImpl(address),
+ "weapon_hegrenade" => new CHEGrenadeImpl(address),
+ "func_guntarget" => new CGunTargetImpl(address),
+ "env_gradient_fog" => new CGradientFogImpl(address),
+ "phys_genericconstraint" => new CGenericConstraintImpl(address),
+ "game_text" => new CGameTextImpl(address),
+ "game_zone_player" => new CGamePlayerZoneImpl(address),
+ "game_player_equip" => new CGamePlayerEquipImpl(address),
+ "game_money" => new CGameMoneyImpl(address),
+ "game_gib_manager" => new CGameGibManagerImpl(address),
+ "game_end" => new CGameEndImpl(address),
+ "func_water" => new CFuncWaterImpl(address),
+ "func_wall_toggle" => new CFuncWallToggleImpl(address),
+ "func_wall" => new CFuncWallImpl(address),
+ "func_vehicleclip" => new CFuncVehicleClipImpl(address),
+ "func_clip_vphysics" => new CFuncVPhysicsClipImpl(address),
+ "func_traincontrols" => new CFuncTrainControlsImpl(address),
+ "func_train" => new CFuncTrainImpl(address),
+ "func_tracktrain" => new CFuncTrackTrainImpl(address),
+ "func_trackchange" => new CFuncTrackChangeImpl(address),
+ "func_trackautochange" => new CFuncTrackAutoImpl(address),
+ "func_timescale" => new CFuncTimescaleImpl(address),
+ "func_tanktrain" => new CFuncTankTrainImpl(address),
+ "func_shatterglass" => new CFuncShatterglassImpl(address),
+ "func_retakebarrier" => new CFuncRetakeBarrierImpl(address),
+ "func_rotator" => new CFuncRotatorImpl(address),
+ "func_rotating" => new CFuncRotatingImpl(address),
+ "func_proprrespawnzone" => new CFuncPropRespawnZoneImpl(address),
+ "func_platrot" => new CFuncPlatRotImpl(address),
+ "func_plat" => new CFuncPlatImpl(address),
+ "func_nav_avoidance_obstacle" => new CFuncNavObstructionImpl(address),
+ "func_nav_blocker" => new CFuncNavBlockerImpl(address),
+ "func_mover" => new CFuncMoverImpl(address),
+ "momentary_door" => new CFuncMoveLinearImpl(address),
+ "func_monitor" => new CFuncMonitorImpl(address),
+ "func_useableladder" => new CFuncLadderImpl(address),
+ "func_illusionary" => new CFuncIllusionaryImpl(address),
+ "func_electrified_volume" => new CFuncElectrifiedVolumeImpl(address),
+ "func_conveyor" => new CFuncConveyorImpl(address),
+ "func_brush" => new CFuncBrushImpl(address),
+ "func_footstep_control" => new CFootstepControlImpl(address),
+ "fog_volume" => new CFogVolumeImpl(address),
+ "trigger_fog" => new CFogTriggerImpl(address),
+ "env_fog_controller" => new CFogControllerImpl(address),
+ "flashbang_projectile" => new CFlashbangProjectileImpl(address),
+ "weapon_flashbang" => new CFlashbangImpl(address),
+ "func_fish_pool" => new CFishPoolImpl(address),
+ "fish" => new CFishImpl(address),
+ "filter_activator_team" => new CFilterTeamImpl(address),
+ "filter_proximity" => new CFilterProximityImpl(address),
+ "filter_activator_name" => new CFilterNameImpl(address),
+ "filter_multi" => new CFilterMultipleImpl(address),
+ "filter_activator_model" => new CFilterModelImpl(address),
+ "filter_activator_mass_greater" => new CFilterMassGreaterImpl(address),
+ "filter_los" => new CFilterLOSImpl(address),
+ "filter_enemy" => new CFilterEnemyImpl(address),
+ "filter_activator_context" => new CFilterContextImpl(address),
+ "filter_activator_class" => new CFilterClassImpl(address),
+ "filter_activator_attribute_int" => new CFilterAttributeIntImpl(address),
+ "env_wind_volume" => new CEnvWindVolumeImpl(address),
+ "env_wind_controller" => new CEnvWindControllerImpl(address),
+ "env_wind" => new CEnvWindImpl(address),
+ "env_volumetric_fog_volume" => new CEnvVolumetricFogVolumeImpl(address),
+ "env_volumetric_fog_controller" => new CEnvVolumetricFogControllerImpl(address),
+ "env_viewpunch" => new CEnvViewPunchImpl(address),
+ "env_tilt" => new CEnvTiltImpl(address),
+ "env_splash" => new CEnvSplashImpl(address),
+ "env_spark" => new CEnvSparkImpl(address),
+ "env_soundscape_triggerable" => new CEnvSoundscapeTriggerableImpl(address),
+ "env_soundscape_proxy" => new CEnvSoundscapeProxyImpl(address),
+ "env_soundscape" => new CEnvSoundscapeImpl(address),
+ "env_sky" => new CEnvSkyImpl(address),
+ "env_shake" => new CEnvShakeImpl(address),
+ "env_particle_glow" => new CEnvParticleGlowImpl(address),
+ "env_muzzleflash" => new CEnvMuzzleFlashImpl(address),
+ "env_light_probe_volume" => new CEnvLightProbeVolumeImpl(address),
+ "env_laser" => new CEnvLaserImpl(address),
+ "env_instructor_vr_hint" => new CEnvInstructorVRHintImpl(address),
+ "env_instructor_hint" => new CEnvInstructorHintImpl(address),
+ "env_hudhint" => new CEnvHudHintImpl(address),
+ "env_global" => new CEnvGlobalImpl(address),
+ "env_fade" => new CEnvFadeImpl(address),
+ "env_explosion" => new CEnvExplosionImpl(address),
+ "env_entity_maker" => new CEnvEntityMakerImpl(address),
+ "env_entity_igniter" => new CEnvEntityIgniterImpl(address),
+ "env_detail_controller" => new CEnvDetailControllerImpl(address),
+ "env_decal" => new CEnvDecalImpl(address),
+ "env_cubemap_fog" => new CEnvCubemapFogImpl(address),
+ "env_cubemap_box" => new CEnvCubemapBoxImpl(address),
+ "env_cubemap" => new CEnvCubemapImpl(address),
+ "env_combined_light_probe_volume" => new CEnvCombinedLightProbeVolumeImpl(address),
+ "env_beverage" => new CEnvBeverageImpl(address),
+ "env_beam" => new CEnvBeamImpl(address),
+ "root" => new CEntityInstanceImpl(address),
+ "entityflame" => new CEntityFlameImpl(address),
+ "env_entity_dissolver" => new CEntityDissolveImpl(address),
+ "entity_blocker" => new CEntityBlockerImpl(address),
+ "point_enable_motion_fixup" => new CEnableMotionFixupImpl(address),
+ "wearable_item" => new CEconWearableImpl(address),
+ "dynamic_prop" => new CDynamicPropImpl(address),
+ "prop_dynamic" => new CDynamicPropImpl(address),
+ "func_nav_dynamic_connections" => new CDynamicNavConnectionsVolumeImpl(address),
+ "light_dynamic" => new CDynamicLightImpl(address),
+ "decoy_projectile" => new CDecoyProjectileImpl(address),
+ "weapon_decoy" => new CDecoyGrenadeImpl(address),
+ "env_debughistory" => new CDebugHistoryImpl(address),
+ "weapon_deagle" => new CDEagleImpl(address),
+ "env_credits" => new CCreditsImpl(address),
+ "info_constraint_anchor" => new CConstraintAnchorImpl(address),
+ "point_commentary_viewpoint" => new CCommentaryViewPositionImpl(address),
+ "commentary_auto" => new CCommentaryAutoImpl(address),
+ "color_correction_volume" => new CColorCorrectionVolumeImpl(address),
+ "color_correction" => new CColorCorrectionImpl(address),
+ "citadel_snd_opvar_set_obb" => new CCitadelSoundOpvarSetOBBImpl(address),
+ "chicken" => new CChickenImpl(address),
+ "trigger_changelevel" => new CChangeLevelImpl(address),
+ "weapon_cs_base" => new CCSWeaponBaseImpl(address),
+ "cs_team_manager" => new CCSTeamImpl(address),
+ "env_sprite_clientside" => new CCSSpriteImpl(address),
+ "cs_player_manager" => new CCSPlayerResourceImpl(address),
+ "env_cs_place" => new CCSPlaceImpl(address),
+ "cs_pet_placement" => new CCSPetPlacementImpl(address),
+ "cs_minimap_boundary" => new CCSMinimapBoundaryImpl(address),
+ "cs_gamerules" => new CCSGameRulesProxyImpl(address),
+ "wingman_intro_terrorist" => new CCSGO_WingmanIntroTerroristPositionImpl(address),
+ "wingman_intro_counterterrorist" => new CCSGO_WingmanIntroCounterTerroristPositionImpl(address),
+ "team_select_terrorist" => new CCSGO_TeamSelectTerroristPositionImpl(address),
+ "team_select_counterterrorist" => new CCSGO_TeamSelectCounterTerroristPositionImpl(address),
+ "team_intro_terrorist" => new CCSGO_TeamIntroTerroristPositionImpl(address),
+ "team_intro_counterterrorist" => new CCSGO_TeamIntroCounterTerroristPositionImpl(address),
+ "weapon_c4" => new CC4Impl(address),
+ "func_buyzone" => new CBuyZoneImpl(address),
+ "func_breakable" => new CBreakableImpl(address),
+ "func_bomb_target" => new CBombTargetImpl(address),
+ "env_blood" => new CBloodImpl(address),
+ "beam" => new CBeamImpl(address),
+ "trigger" => new CBaseTriggerImpl(address),
+ "baseplayerpawn" => new CBasePlayerPawnImpl(address),
+ "player_controller" => new CBasePlayerControllerImpl(address),
+ "move_keyframed" => new CBaseMoveBehaviorImpl(address),
+ "basemodelentity" => new CBaseModelEntityImpl(address),
+ "grenade" => new CBaseGrenadeImpl(address),
+ "baseflex" => new CBaseFlexImpl(address),
+ "filter_base" => new CBaseFilterImpl(address),
+ "func_door" => new CBaseDoorImpl(address),
+ "info_player_deathmatch" => new CBaseDMStartImpl(address),
+ "weapon_basecsgrenade" => new CBaseCSGrenadeImpl(address),
+ "func_button" => new CBaseButtonImpl(address),
+ "baseanimgraph" => new CBaseAnimGraphImpl(address),
+ "light_barn" => new CBarnLightImpl(address),
+ "ambient_generic" => new CAmbientGenericImpl(address),
+ "weapon_ak47" => new CAK47Impl(address),
+ "ai_changehintgroup" => new CAI_ChangeHintGroupImpl(address),
+ _ => new CEntityInstanceImpl(address),
+ };
+ }
+}
\ No newline at end of file
diff --git a/managed/src/SwiftlyS2.Core/Modules/EntitySystem/EntityManager.cs b/managed/src/SwiftlyS2.Core/Modules/EntitySystem/EntityManager.cs
new file mode 100644
index 000000000..a3608d473
--- /dev/null
+++ b/managed/src/SwiftlyS2.Core/Modules/EntitySystem/EntityManager.cs
@@ -0,0 +1,108 @@
+using SwiftlyS2.Core.SchemaDefinitions;
+using SwiftlyS2.Shared.SchemaDefinitions;
+
+namespace SwiftlyS2.Core.EntitySystem;
+
+internal static class EntityManager
+{
+ private static readonly CEntityInstance?[] _Entities = new CEntityInstance?[1 << 15];
+ private static readonly List _ActiveEntityIndices = [];
+ private static readonly Dictionary _PtrToIndex = [];
+ private static readonly CEntityInstanceImpl _Dummy = new(0);
+ private static readonly ReaderWriterLockSlim _rw = new(LockRecursionPolicy.NoRecursion);
+
+ public static CEntityInstance OnEntityCreated( nint entityPtr )
+ {
+ var ent = GetEntityByAddress(entityPtr);
+ if (ent != null) return ent;
+
+ _Dummy.DangerousSetHandle(entityPtr);
+ var index = _Dummy.Index;
+ var entity = ClassConvertor.ConvertEntityByDesignerName(entityPtr, _Dummy.DesignerName);
+ _rw.EnterWriteLock();
+ try
+ {
+ _Entities[index] = entity;
+ _ActiveEntityIndices.Add(index);
+ _PtrToIndex.Add(entityPtr, index);
+ return entity;
+ }
+ finally
+ {
+ _rw.ExitWriteLock();
+ }
+ }
+
+ public static CEntityInstance? GetEntityByIndex( uint index )
+ {
+ _rw.EnterReadLock();
+ try
+ {
+ return _Entities[index];
+ }
+ finally
+ {
+ _rw.ExitReadLock();
+ }
+ }
+
+ public static CEntityInstance? GetEntityByAddress( nint address )
+ {
+ _rw.EnterReadLock();
+ try
+ {
+ return !_PtrToIndex.TryGetValue(address, out var value) ? null : _Entities[value];
+ }
+ finally
+ {
+ _rw.ExitReadLock();
+ }
+ }
+
+ public static void OnEntityDeleted( nint entityPtr )
+ {
+ _rw.EnterWriteLock();
+ try
+ {
+ if (!_PtrToIndex.TryGetValue(entityPtr, out var index))
+ {
+ return;
+ }
+ _Entities[index] = null;
+ _ = _ActiveEntityIndices.Remove(index);
+ _ = _PtrToIndex.Remove(entityPtr);
+ }
+ finally
+ {
+ _rw.ExitWriteLock();
+ }
+ }
+
+ public static IEnumerable GetAllEntities()
+ {
+
+ _rw.EnterReadLock();
+ try
+ {
+ return _ActiveEntityIndices.Select(index => _Entities[index]!);
+ }
+ finally
+ {
+ _rw.ExitReadLock();
+ }
+ }
+
+ public static bool IsAddressValid( nint address )
+ {
+ _rw.EnterReadLock();
+ try
+ {
+ return _PtrToIndex.ContainsKey(address);
+ }
+ finally
+ {
+ _rw.ExitReadLock();
+ }
+ }
+
+}
diff --git a/managed/src/SwiftlyS2.Core/Modules/EntitySystem/EntityOutputHookCallback.cs b/managed/src/SwiftlyS2.Core/Modules/EntitySystem/EntityOutputHookCallback.cs
deleted file mode 100644
index 38f9ce18e..000000000
--- a/managed/src/SwiftlyS2.Core/Modules/EntitySystem/EntityOutputHookCallback.cs
+++ /dev/null
@@ -1,88 +0,0 @@
-using System.Runtime.InteropServices;
-using System.Runtime.CompilerServices;
-using Microsoft.Extensions.Logging;
-using SwiftlyS2.Shared.Misc;
-using SwiftlyS2.Core.Natives;
-using SwiftlyS2.Shared.Natives;
-using SwiftlyS2.Shared.Profiler;
-using SwiftlyS2.Shared.EntitySystem;
-using SwiftlyS2.Core.SchemaDefinitions;
-
-namespace SwiftlyS2.Core.NetMessages;
-
-[Obsolete("Use HookEntityOutput with EntityOutputEventHandler instead.")]
-[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
-internal delegate int EntityOutputHookCallbackDelegate( nint entityio, nint outputName, nint activator, nint caller, float delay );
-
-[Obsolete("Use HookEntityOutput with EntityOutputEventHandler instead.")]
-internal class EntityOutputHookCallback : IDisposable
-{
- public Guid Guid { get; init; }
-
- private readonly ILogger logger;
- private readonly EntityOutputHookCallbackDelegate unmanagedCallback;
- private readonly nint unmanagedCallbackPtr;
- private readonly ulong nativeHookId;
-
- private volatile bool disposed;
-
- public EntityOutputHookCallback( string className, string outputName, IEntitySystemService.EntityOutputHandler callback, ILoggerFactory loggerFactory, IContextedProfilerService profiler )
- {
- this.Guid = Guid.NewGuid();
- this.logger = loggerFactory.CreateLogger();
- this.disposed = false;
-
- unmanagedCallback = ( entityio, outputName, activator, caller, delay ) =>
- {
- try
- {
- var category = "EntityOutputHookCallback::" + outputName;
- profiler.StartRecording(category);
- var outputStr = Marshal.PtrToStringAnsi(outputName) ?? string.Empty;
- HookResult result;
- unsafe
- {
- result = callback(
- Unsafe.AsRef((void*)entityio),
- Marshal.PtrToStringAnsi(outputName) ?? string.Empty,
- new CEntityInstanceImpl(activator),
- new CEntityInstanceImpl(caller),
- delay
- );
- }
- profiler.StopRecording(category);
- return (int)result;
- }
- catch (Exception e)
- {
- if (!GlobalExceptionHandler.Handle(e))
- {
- return 0;
- }
- logger.LogError(e, "Failed to execute entity output callback {0}.", Guid);
- }
- return 0;
- };
-
- unmanagedCallbackPtr = Marshal.GetFunctionPointerForDelegate(unmanagedCallback);
- nativeHookId = NativeEntitySystem.HookEntityOutput(className, outputName, unmanagedCallbackPtr);
- }
-
- ~EntityOutputHookCallback()
- {
- Dispose();
- }
-
- public void Dispose()
- {
- if (disposed)
- {
- return;
- }
- disposed = true;
-
- NativeEntitySystem.UnhookEntityOutput(nativeHookId);
-
- GC.SuppressFinalize(this);
- }
-}
\ No newline at end of file
diff --git a/managed/src/SwiftlyS2.Core/Modules/EntitySystem/EntitySystem.cs b/managed/src/SwiftlyS2.Core/Modules/EntitySystem/EntitySystem.cs
index f0a598edd..b003a13c9 100644
--- a/managed/src/SwiftlyS2.Core/Modules/EntitySystem/EntitySystem.cs
+++ b/managed/src/SwiftlyS2.Core/Modules/EntitySystem/EntitySystem.cs
@@ -1,13 +1,10 @@
using System.Collections.Concurrent;
-using Microsoft.Extensions.Logging;
using SwiftlyS2.Core.Natives;
using SwiftlyS2.Shared.Events;
using SwiftlyS2.Shared.Natives;
using SwiftlyS2.Shared.Schemas;
using SwiftlyS2.Core.Extensions;
-using SwiftlyS2.Shared.Profiler;
-using SwiftlyS2.Core.NetMessages;
using SwiftlyS2.Shared.EntitySystem;
using SwiftlyS2.Core.SchemaDefinitions;
using SwiftlyS2.Shared.SchemaDefinitions;
@@ -16,21 +13,15 @@ namespace SwiftlyS2.Core.EntitySystem;
internal class EntitySystemService : IEntitySystemService, IDisposable
{
- private readonly ILoggerFactory loggerFactory;
- private readonly IContextedProfilerService profiler;
private readonly IEventSubscriber eventSubscriber;
- [Obsolete("Use outputHooks instead.")]
- private readonly ConcurrentDictionary outputCallbacks = new();
private readonly ConcurrentDictionary outputHooks = new();
private readonly ConcurrentDictionary inputHooks = new();
private volatile bool disposed;
- public EntitySystemService( IEventSubscriber eventSubscriber, ILoggerFactory loggerFactory, IContextedProfilerService profiler )
+ public EntitySystemService( IEventSubscriber eventSubscriber )
{
- this.loggerFactory = loggerFactory;
- this.profiler = profiler;
this.eventSubscriber = eventSubscriber;
this.disposed = false;
}
@@ -51,13 +42,20 @@ public T CreateEntity() where T : class, ISchemaClass
: CreateEntityByDesignerName(T.ClassName);
}
- public T CreateEntityByDesignerName( string designerName ) where T : ISchemaClass
+ public T CreateEntityByDesignerName( string designerName ) where T : class, ISchemaClass
+ {
+ return (CreateEntityByDesignerName(designerName) as T)!;
+ }
+
+ public CEntityInstance CreateEntityByDesignerName( string designerName )
{
ThrowIfEntitySystemInvalid();
var handle = NativeEntitySystem.CreateEntityByName(designerName);
+ var entity = EntityManager.OnEntityCreated(handle);
+
return handle == nint.Zero
? throw new ArgumentException($"Failed to create entity by designer name: {designerName}, probably invalid designer name.")
- : T.From(handle);
+ : entity;
}
public CHandle GetRefEHandle( T entity ) where T : class, ISchemaClass
@@ -75,45 +73,37 @@ public CHandle GetRefEHandle( T entity ) where T : class, ISchemaClass
public IEnumerable GetAllEntities()
{
- ThrowIfEntitySystemInvalid();
- CEntityIdentity? pFirst = new CEntityIdentityImpl(NativeEntitySystem.GetFirstActiveEntity());
-
- while (pFirst != null && pFirst.IsValid)
- {
- yield return new CEntityInstanceImpl(pFirst.Address.Read());
- pFirst = pFirst.Next;
- }
+ return EntityManager.GetAllEntities();
}
public IEnumerable GetAllEntitiesByClass() where T : class, ISchemaClass
{
- ThrowIfEntitySystemInvalid();
- return string.IsNullOrWhiteSpace(T.ClassName)
- ? throw new ArgumentException($"Can't get entities with class {typeof(T).Name}, which doesn't have a designer name")
- : GetAllEntities().Where(( entity ) => entity.Entity?.DesignerName == T.ClassName).Select(( entity ) => T.From(entity.Address));
+ return GetAllEntities().OfType();
}
public IEnumerable GetAllEntitiesByDesignerName( string designerName ) where T : class, ISchemaClass
{
- ThrowIfEntitySystemInvalid();
return GetAllEntities()
.Where(entity => entity.Entity?.DesignerName == designerName)
- .Select(entity => T.From(entity.Address));
+ .Select(entity => (entity as T)!);
}
public T? GetEntityByIndex( uint index ) where T : class, ISchemaClass
{
- ThrowIfEntitySystemInvalid();
- var handle = NativeEntitySystem.GetEntityByIndex(index);
- return handle == nint.Zero ? null : T.From(handle);
+ var ent = GetEntityByIndex(index);
+ if (ent is null)
+ {
+ return null;
+ }
+
+ return ent is T e
+ ? e
+ : throw new InvalidOperationException($"Invalid entity type. Requested: {typeof(T).Name}, Actual: {ent!.GetType().Name}.");
}
- [Obsolete("Use HookEntityOutput(string outputName, Action callback) instead.")]
- public Guid HookEntityOutput( string outputName, IEntitySystemService.EntityOutputHandler callback ) where T : class, ISchemaClass
+ public CEntityInstance? GetEntityByIndex( uint index )
{
- var hook = new EntityOutputHookCallback(T.ClassName ?? throw new ArgumentException($"Can't hook entity output with class {typeof(T).Name}, which doesn't have a designer name"), outputName, callback, loggerFactory, profiler);
- _ = outputCallbacks.TryAdd(hook.Guid, hook);
- return hook.Guid;
+ return EntityManager.GetEntityByIndex(index);
}
public Guid HookEntityOutput( string outputName, IEntitySystemService.EntityOutputEventHandler callback ) where T : class, ISchemaClass
@@ -242,12 +232,7 @@ void handler( IOnEntityIdentityAcceptInputHookEvent @event )
public bool UnhookEntityOutput( Guid guid )
{
- if (outputCallbacks.TryRemove(guid, out var callback))
- {
- callback.Dispose();
- return true;
- }
- else if (outputHooks.TryRemove(guid, out var handler))
+ if (outputHooks.TryRemove(guid, out var handler))
{
eventSubscriber.OnEntityFireOutputHook -= handler;
return true;
@@ -273,12 +258,6 @@ public void Dispose()
}
disposed = true;
- foreach (var callback in outputCallbacks.Values)
- {
- callback.Dispose();
- }
- outputCallbacks.Clear();
-
foreach (var handler in outputHooks.Values)
{
eventSubscriber.OnEntityFireOutputHook -= handler;
@@ -294,6 +273,28 @@ public void Dispose()
GC.SuppressFinalize(this);
}
+ public T? GetEntityByAddress( nint address ) where T : class, ISchemaClass
+ {
+ var ent = GetEntityByAddress(address);
+ if (ent is null)
+ {
+ return null;
+ }
+ if (ent is T e)
+ {
+ return e;
+ }
+ else
+ {
+ throw new InvalidOperationException($"Invalid entity type. Requested: {typeof(T).Name}, Actual: {ent!.GetType().Name}.");
+ }
+ }
+
+ public CEntityInstance? GetEntityByAddress( nint address )
+ {
+ return EntityManager.GetEntityByAddress(address);
+ }
+
~EntitySystemService()
{
Dispose();
diff --git a/managed/src/SwiftlyS2.Core/Modules/Events/EventParams/OnClientVoiceEvent.cs b/managed/src/SwiftlyS2.Core/Modules/Events/EventParams/OnClientVoiceEvent.cs
new file mode 100644
index 000000000..a3dd9ee06
--- /dev/null
+++ b/managed/src/SwiftlyS2.Core/Modules/Events/EventParams/OnClientVoiceEvent.cs
@@ -0,0 +1,9 @@
+using SwiftlyS2.Shared.Events;
+
+namespace SwiftlyS2.Core.Events;
+
+
+internal class OnClientVoiceEvent : IOnClientVoiceEvent
+{
+ public int PlayerId { get; set; }
+}
diff --git a/managed/src/SwiftlyS2.Core/Modules/Events/EventParams/OnEntityTouchHookEvent.cs b/managed/src/SwiftlyS2.Core/Modules/Events/EventParams/OnEntityTouchHookEvent.cs
index b20a4eb9e..1ecb500bb 100644
--- a/managed/src/SwiftlyS2.Core/Modules/Events/EventParams/OnEntityTouchHookEvent.cs
+++ b/managed/src/SwiftlyS2.Core/Modules/Events/EventParams/OnEntityTouchHookEvent.cs
@@ -3,16 +3,6 @@
namespace SwiftlyS2.Core.Events;
-[Obsolete("OnEntityTouchHookEvent is deprecated. Use OnEntityStartTouchEvent, OnEntityTouchEvent, or OnEntityEndTouchEvent instead.")]
-internal class OnEntityTouchHookEvent : IOnEntityTouchHookEvent
-{
- public required CBaseEntity Entity { get; init; }
-
- public required CBaseEntity OtherEntity { get; init; }
-
- public required EntityTouchType TouchType { get; init; }
-}
-
internal class OnEntityStartTouchEvent : IOnEntityStartTouchEvent
{
public required CBaseEntity Entity { get; init; }
diff --git a/managed/src/SwiftlyS2.Core/Modules/Events/EventParams/OnWeaponServicesDropWeaponHook.cs b/managed/src/SwiftlyS2.Core/Modules/Events/EventParams/OnWeaponServicesDropWeaponHook.cs
new file mode 100644
index 000000000..a3bf0bbd1
--- /dev/null
+++ b/managed/src/SwiftlyS2.Core/Modules/Events/EventParams/OnWeaponServicesDropWeaponHook.cs
@@ -0,0 +1,16 @@
+using SwiftlyS2.Shared.Events;
+using SwiftlyS2.Shared.Misc;
+using SwiftlyS2.Shared.SchemaDefinitions;
+
+namespace SwiftlyS2.Core.Events;
+
+internal class OnWeaponServicesDropWeaponHook : IOnWeaponServicesDropWeaponHook
+{
+
+ public required CCSPlayer_WeaponServices WeaponServices { get; set; }
+
+ public required CBasePlayerWeapon? Weapon { get; set; }
+
+ public required bool SwappingWeapon { get; set; }
+ public required HookResult Result { get; set; } = HookResult.Continue;
+}
\ No newline at end of file
diff --git a/managed/src/SwiftlyS2.Core/Modules/Events/EventPublisher.cs b/managed/src/SwiftlyS2.Core/Modules/Events/EventPublisher.cs
index e1ac9875c..c19e3dad7 100644
--- a/managed/src/SwiftlyS2.Core/Modules/Events/EventPublisher.cs
+++ b/managed/src/SwiftlyS2.Core/Modules/Events/EventPublisher.cs
@@ -6,14 +6,14 @@
using SwiftlyS2.Core.Scheduler;
using SwiftlyS2.Core.SchemaDefinitions;
using SwiftlyS2.Core.ProtobufDefinitions;
-using SwiftlyS2.Shared.SchemaDefinitions;
using SwiftlyS2.Shared.ProtobufDefinitions;
+using SwiftlyS2.Core.Players;
+using SwiftlyS2.Core.EntitySystem;
namespace SwiftlyS2.Core.Events;
internal static class EventPublisher
{
- internal static event Action? InternalOnMapLoad;
private static readonly List subscribers = [];
private static readonly Lock subscribersLock = new();
@@ -55,6 +55,7 @@ public static void Register()
NativeEvents.RegisterOnEntityTakeDamageCallback((nint)(delegate* unmanaged< nint, nint, nint, byte >)&OnEntityTakeDamage);
NativeEvents.RegisterOnPrecacheResourceCallback((nint)(delegate* unmanaged< nint, void >)&OnPrecacheResource);
NativeEvents.RegisterOnStartupServerCallback((nint)(delegate* unmanaged< void >)&OnStartupServer);
+ NativeEvents.RegisterOnClientVoiceCallback((nint)(delegate* unmanaged< int, void >)&OnClientVoice);
_ = NativeConvars.AddConvarCreatedListener((nint)(delegate* unmanaged< nint, void >)&OnConVarCreated);
_ = NativeConvars.AddConCommandCreatedListener((nint)(delegate* unmanaged< nint, void >)&OnConCommandCreated);
_ = NativeConvars.AddGlobalChangeListener((nint)(delegate* unmanaged< nint, int, nint, nint, void >)&OnConVarValueChanged);
@@ -203,6 +204,7 @@ public static void OnPreworldUpdate( byte simulating )
[UnmanagedCallersOnly]
public static byte OnClientConnected( int playerId )
{
+ PlayerManagerService.RegisterPlayerObject(playerId);
if (subscribers.Count == 0)
{
return 1;
@@ -222,6 +224,7 @@ public static byte OnClientConnected( int playerId )
if (@event.Result == HookResult.Stop)
{
+ PlayerManagerService.UnregisterPlayerObject(playerId);
return 0;
}
}
@@ -257,13 +260,19 @@ public static void OnClientDisconnected( int playerId, int reason )
{
subscriber.InvokeOnClientDisconnected(@event);
}
+
+ PlayerManagerService.UnregisterPlayerObject(playerId);
+
}
catch (Exception e)
{
if (!GlobalExceptionHandler.Handle(e))
{
+ PlayerManagerService.UnregisterPlayerObject(playerId);
return;
}
+
+ PlayerManagerService.UnregisterPlayerObject(playerId);
AnsiConsole.WriteException(e);
}
}
@@ -306,6 +315,8 @@ public static void OnClientPutInServer( int playerId, int clientKind )
return;
}
+ if (clientKind == (int)ClientKind.Bot) PlayerManagerService.RegisterPlayerObject(playerId);
+
try
{
OnClientPutInServerEvent @event = new() {
@@ -382,6 +393,7 @@ public static void OnClientSteamAuthorizeFail( int playerId )
[UnmanagedCallersOnly]
public static void OnEntityCreated( nint entityPtr )
{
+ var entity = EntityManager.OnEntityCreated(entityPtr);
if (subscribers.Count == 0)
{
return;
@@ -389,7 +401,7 @@ public static void OnEntityCreated( nint entityPtr )
try
{
- OnEntityCreatedEvent @event = new() { Entity = new CEntityInstanceImpl(entityPtr) };
+ OnEntityCreatedEvent @event = new() { Entity = entity };
foreach (var subscriber in subscribers)
{
subscriber.InvokeOnEntityCreated(@event);
@@ -410,12 +422,15 @@ public static void OnEntityDeleted( nint entityPtr )
{
if (subscribers.Count == 0)
{
+ EntityManager.OnEntityDeleted(entityPtr);
return;
}
+ var entity = EntityManager.GetEntityByAddress(entityPtr);
+
try
{
- OnEntityDeletedEvent @event = new() { Entity = new CEntityInstanceImpl(entityPtr) };
+ OnEntityDeletedEvent @event = new() { Entity = entity! };
foreach (var subscriber in subscribers)
{
subscriber.InvokeOnEntityDeleted(@event);
@@ -429,6 +444,10 @@ public static void OnEntityDeleted( nint entityPtr )
}
AnsiConsole.WriteException(e);
}
+ finally
+ {
+ EntityManager.OnEntityDeleted(entityPtr);
+ }
}
[UnmanagedCallersOnly]
@@ -496,7 +515,11 @@ public static void OnMapLoad( nint mapNamePtr )
try
{
- InternalOnMapLoad?.Invoke(); // calls before all plugins.
+ OnMapLoadEvent @event = new() { MapName = Marshal.PtrToStringUTF8(mapNamePtr) ?? string.Empty };
+ foreach (var subscriber in subscribers)
+ {
+ subscriber.InvokeOnMapLoad(@event);
+ }
}
catch (Exception e)
{
@@ -506,13 +529,22 @@ public static void OnMapLoad( nint mapNamePtr )
}
AnsiConsole.WriteException(e);
}
+ }
+
+ [UnmanagedCallersOnly]
+ public static void OnClientVoice( int playerId )
+ {
+ if (subscribers.Count == 0)
+ {
+ return;
+ }
try
{
- OnMapLoadEvent @event = new() { MapName = Marshal.PtrToStringUTF8(mapNamePtr) ?? string.Empty };
+ OnClientVoiceEvent @event = new() { PlayerId = playerId };
foreach (var subscriber in subscribers)
{
- subscriber.InvokeOnMapLoad(@event);
+ subscriber.InvokeOnClientVoice(@event);
}
}
catch (Exception e)
@@ -689,31 +721,6 @@ public static void OnStartupServer()
}
}
- [Obsolete("InvokeOnEntityTouchHook is deprecated. Use InvokeOnEntityStartTouch, InvokeOnEntityTouch, or InvokeOnEntityEndTouch instead.")]
- public static void InvokeOnEntityTouchHook( OnEntityTouchHookEvent @event )
- {
- if (subscribers.Count == 0)
- {
- return;
- }
-
- try
- {
- foreach (var subscriber in subscribers)
- {
- subscriber.InvokeOnEntityTouchHook(@event);
- }
- }
- catch (Exception e)
- {
- if (!GlobalExceptionHandler.Handle(e))
- {
- return;
- }
- AnsiConsole.WriteException(e);
- }
- }
-
public static void InvokeOnEntityStartTouch( OnEntityStartTouchEvent @event )
{
if (subscribers.Count == 0)
@@ -984,6 +991,30 @@ public static void InvokeOnEntityIdentityAcceptInputHook( OnEntityIdentityAccept
}
}
+ public static void InvokeOnWeaponServicesDropWeaponHook( OnWeaponServicesDropWeaponHook @event )
+ {
+ if (subscribers.Count == 0)
+ {
+ return;
+ }
+
+ try
+ {
+ foreach (var subscriber in subscribers)
+ {
+ subscriber.InvokeOnWeaponServicesDropWeaponHook(@event);
+ }
+ }
+ catch (Exception e)
+ {
+ if (!GlobalExceptionHandler.Handle(e))
+ {
+ return;
+ }
+ AnsiConsole.WriteException(e);
+ }
+ }
+
public static void InvokeEntityFireOutputHook( OnEntityFireOutputHookEvent @event )
{
if (subscribers.Count == 0)
diff --git a/managed/src/SwiftlyS2.Core/Modules/Events/EventSubscriber.cs b/managed/src/SwiftlyS2.Core/Modules/Events/EventSubscriber.cs
index 5fef525cd..b2a52fd32 100644
--- a/managed/src/SwiftlyS2.Core/Modules/Events/EventSubscriber.cs
+++ b/managed/src/SwiftlyS2.Core/Modules/Events/EventSubscriber.cs
@@ -49,12 +49,12 @@ public EventSubscriber( IContextedProfilerService profiler, ILogger FindFileAbsoluteList( string wildcard, string pathId )
{
- List results = new();
+ List results = [];
ManagedCUtlVector files = new();
- unsafe {
- fixed (void* filesPtr = &files.Value) {
+ unsafe
+ {
+ fixed (void* filesPtr = &files.Value)
+ {
NativeFileSystem.FindFileAbsoluteList(new IntPtr(filesPtr), wildcard, pathId);
}
diff --git a/managed/src/SwiftlyS2.Core/Modules/Game/GameService.cs b/managed/src/SwiftlyS2.Core/Modules/Game/GameService.cs
index 4b036787a..cd9abb77b 100644
--- a/managed/src/SwiftlyS2.Core/Modules/Game/GameService.cs
+++ b/managed/src/SwiftlyS2.Core/Modules/Game/GameService.cs
@@ -192,17 +192,9 @@ public unsafe int GetWinningTeam()
}
}
- if (match->TerroristScoreTotal > match->CTScoreTotal)
- {
- return (int)Team.T;
- }
-
- if (match->TerroristScoreTotal < match->CTScoreTotal)
- {
- return (int)Team.CT;
- }
-
- return (int)Team.None;
+ return match->TerroristScoreTotal > match->CTScoreTotal
+ ? (int)Team.T
+ : match->TerroristScoreTotal < match->CTScoreTotal ? (int)Team.CT : (int)Team.None;
}
private unsafe void UpdateTeamScores()
@@ -221,6 +213,8 @@ private unsafe void UpdateTeamScores()
case (int)Team.CT:
UpdateTeamEntity(team, match->CTScoreTotal, match->CTScoreFirstHalf, match->CTScoreSecondHalf, match->CTScoreOvertime);
break;
+ default:
+ break;
}
}
}
@@ -228,11 +222,7 @@ private unsafe void UpdateTeamScores()
private CCSGameRules GetGameRules()
{
var gameRules = entitySystemService.GetGameRules();
- if (gameRules?.IsValid ?? false)
- {
- return gameRules;
- }
- throw new InvalidOperationException("GameRules not found");
+ return gameRules?.IsValid ?? false ? gameRules : throw new InvalidOperationException("GameRules not found");
}
private unsafe CCSMatch* GetCCSMatchPtr()
diff --git a/managed/src/SwiftlyS2.Core/Modules/GameData/GameDataService.cs b/managed/src/SwiftlyS2.Core/Modules/GameData/GameDataService.cs
index e05cafd10..bd400eb4c 100644
--- a/managed/src/SwiftlyS2.Core/Modules/GameData/GameDataService.cs
+++ b/managed/src/SwiftlyS2.Core/Modules/GameData/GameDataService.cs
@@ -17,185 +17,137 @@ internal record Patch( string signature, string windows, string linux );
internal class GameDataService : IGameDataService
{
- private CoreContext _Context { get; init; }
+ private CoreContext _Context { get; init; }
- private ConcurrentDictionary _Signatures = new();
- private ConcurrentDictionary _Offsets = new();
- private ConcurrentDictionary _Patches = new();
+ private ConcurrentDictionary _Signatures = [];
+ private ConcurrentDictionary _Offsets = [];
+ private ConcurrentDictionary _Patches = [];
- private static OSPlatform _Platform = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? OSPlatform.Windows : OSPlatform.Linux;
+ private static readonly OSPlatform _Platform = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? OSPlatform.Windows : OSPlatform.Linux;
- public GameDataService( CoreContext context, MemoryService memoryService, ILogger logger )
- {
- _Context = context;
-
- var signaturePath = Path.Combine(_Context.BaseDirectory, "resources", "gamedata", "signatures.jsonc");
- var offsetPath = Path.Combine(_Context.BaseDirectory, "resources", "gamedata", "offsets.jsonc");
- var patchPath = Path.Combine(_Context.BaseDirectory, "resources", "gamedata", "patches.jsonc");
+ public GameDataService( CoreContext context, MemoryService memoryService, ILogger logger )
+ {
+ _Context = context;
- var options = new JsonSerializerOptions() {
- AllowTrailingCommas = true,
- ReadCommentHandling = JsonCommentHandling.Skip,
- };
+ var signaturePath = Path.Combine(_Context.BaseDirectory, "resources", "gamedata", "signatures.jsonc");
+ var offsetPath = Path.Combine(_Context.BaseDirectory, "resources", "gamedata", "offsets.jsonc");
+ var patchPath = Path.Combine(_Context.BaseDirectory, "resources", "gamedata", "patches.jsonc");
- try
- {
+ var options = new JsonSerializerOptions() {
+ AllowTrailingCommas = true,
+ ReadCommentHandling = JsonCommentHandling.Skip,
+ };
- if (File.Exists(signaturePath))
- {
- var signatures = JsonSerializer.Deserialize>(File.ReadAllText(signaturePath), options)!;
- foreach (var signature in signatures)
+ try
{
- nint? value = null;
- if (_Platform == OSPlatform.Windows)
- {
- value = memoryService.GetAddressBySignature(signature.Value.lib, signature.Value.windows);
- }
- else
- {
- value = memoryService.GetAddressBySignature(signature.Value.lib, signature.Value.linux);
- }
- if (value is null)
- {
- logger.LogError("Failed to load signature {Signature}!", signature.Key);
- continue;
- }
- _Signatures.TryAdd(signature.Key, value.Value);
- }
- }
- if (File.Exists(offsetPath))
- {
- var offsets = JsonSerializer.Deserialize>(File.ReadAllText(offsetPath), options)!;
- foreach (var offset in offsets)
- {
- if (_Platform == OSPlatform.Windows)
- {
- _Offsets.TryAdd(offset.Key, offset.Value.windows);
- }
- else
- {
- _Offsets.TryAdd(offset.Key, offset.Value.linux);
- }
- }
- }
+ if (File.Exists(signaturePath))
+ {
+ var signatures = JsonSerializer.Deserialize>(File.ReadAllText(signaturePath), options)!;
+ foreach (var signature in signatures)
+ {
+ var value = memoryService.GetAddressBySignature(signature.Value.lib, _Platform == OSPlatform.Windows ? signature.Value.windows : signature.Value.linux);
+ if (value is null)
+ {
+ logger.LogError("Failed to load signature {Signature}!", signature.Key);
+ continue;
+ }
+ _ = _Signatures.TryAdd(signature.Key, value.Value);
+ }
+ }
+
+ if (File.Exists(offsetPath))
+ {
+ var offsets = JsonSerializer.Deserialize>(File.ReadAllText(offsetPath), options)!;
+ foreach (var offset in offsets)
+ {
+ _ = _Offsets.TryAdd(offset.Key, _Platform == OSPlatform.Windows ? offset.Value.windows : offset.Value.linux);
+ }
+ }
+
+ if (File.Exists(patchPath))
+ {
+ var patches = JsonSerializer.Deserialize>(File.ReadAllText(patchPath), options)!;
+ foreach (var patch in patches)
+ {
+ _ = _Patches.TryAdd(patch.Key, patch.Value);
+ }
+ }
- if (File.Exists(patchPath))
- {
- var patches = JsonSerializer.Deserialize>(File.ReadAllText(patchPath), options)!;
- foreach (var patch in patches)
+ }
+ catch (Exception e)
{
- _Patches.TryAdd(patch.Key, patch.Value);
+ if (!GlobalExceptionHandler.Handle(e)) return;
+ logger.LogError(e, "Failed to load game data.");
}
- }
-
- }
- catch (Exception e)
- {
- if (!GlobalExceptionHandler.Handle(e)) return;
- logger.LogError(e, "Failed to load game data.");
}
- }
- public bool HasSignature( string signatureName )
- {
- if (_Signatures.ContainsKey(signatureName))
+ public bool HasSignature( string signatureName )
{
- return true;
+ return _Signatures.ContainsKey(signatureName) || NativeSignatures.Exists(signatureName);
}
- return NativeSignatures.Exists(signatureName);
- }
-
- public nint GetSignature( string signatureName )
- {
- if (_Signatures.TryGetValue(signatureName, out var signature))
+ public nint GetSignature( string signatureName )
{
- return signature;
+ return _Signatures.TryGetValue(signatureName, out var signature) ? signature : NativeSignatures.Fetch(signatureName);
}
- return NativeSignatures.Fetch(signatureName);
- }
- public bool TryGetSignature( string signatureName, out nint signature )
- {
- if (_Signatures.TryGetValue(signatureName, out var _signature))
+ public bool TryGetSignature( string signatureName, out nint signature )
{
- signature = _signature;
- return true;
+ if (_Signatures.TryGetValue(signatureName, out var _signature))
+ {
+ signature = _signature;
+ return true;
+ }
+ signature = NativeSignatures.Fetch(signatureName);
+ return signature != nint.Zero;
}
- signature = NativeSignatures.Fetch(signatureName);
- return signature != nint.Zero;
- }
- public bool HasOffset( string offsetName )
- {
- if (_Offsets.ContainsKey(offsetName))
+ public bool HasOffset( string offsetName )
{
- return true;
+ return _Offsets.ContainsKey(offsetName) || NativeOffsets.Exists(offsetName);
}
- return NativeOffsets.Exists(offsetName);
- }
-
- public int GetOffset( string offsetName )
- {
- if (_Offsets.TryGetValue(offsetName, out var offset))
+ public int GetOffset( string offsetName )
{
- return offset;
+ return _Offsets.TryGetValue(offsetName, out var offset) ? offset : NativeOffsets.Fetch(offsetName);
}
- return NativeOffsets.Fetch(offsetName);
- }
- public bool TryGetOffset( string offsetName, out nint offset )
- {
- if (_Offsets.TryGetValue(offsetName, out var _offset))
+ public bool TryGetOffset( string offsetName, out nint offset )
{
- offset = _offset;
- return true;
+ if (_Offsets.TryGetValue(offsetName, out var _offset))
+ {
+ offset = _offset;
+ return true;
+ }
+ offset = NativeOffsets.Fetch(offsetName);
+ return offset != nint.Zero;
}
- offset = NativeOffsets.Fetch(offsetName);
- return offset != nint.Zero;
- }
- public bool HasPatch( string patchName )
- {
- if (_Patches.ContainsKey(patchName))
+ public bool HasPatch( string patchName )
{
- return true;
+ return _Patches.ContainsKey(patchName) || NativePatches.Exists(patchName);
}
- return NativePatches.Exists(patchName);
- }
- public void ApplyPatch( string patchName )
- {
- if (_Patches.TryGetValue(patchName, out var patch))
+ public void ApplyPatch( string patchName )
{
- nint address = GetSignature(patch.signature);
- if (address == nint.Zero)
- {
- throw new Exception($"Failed to apply patch {patchName}, cannot find signature {patch.signature}.");
- }
-
- byte[] bytes;
-
- if (_Platform == OSPlatform.Windows)
- {
- bytes = patch.windows
- .Split(' ', StringSplitOptions.RemoveEmptyEntries)
- .Select(x => byte.Parse(x, NumberStyles.HexNumber, CultureInfo.InvariantCulture))
- .ToArray();
- }
- else
- {
- bytes = patch.linux
- .Split(' ', StringSplitOptions.RemoveEmptyEntries)
- .Select(x => byte.Parse(x, NumberStyles.HexNumber, CultureInfo.InvariantCulture))
- .ToArray();
- }
- MemoryPatch.SetMemAccess(address, bytes.Length);
- address.CopyFrom(bytes);
- return;
+ if (_Patches.TryGetValue(patchName, out var patch))
+ {
+ var address = GetSignature(patch.signature);
+ if (address == nint.Zero)
+ {
+ throw new Exception($"Failed to apply patch {patchName}, cannot find signature {patch.signature}.");
+ }
+
+ var bytes = (_Platform == OSPlatform.Windows ? patch.windows : patch.linux)
+ .Split(' ', StringSplitOptions.RemoveEmptyEntries)
+ .Select(x => byte.Parse(x, NumberStyles.HexNumber, CultureInfo.InvariantCulture))
+ .ToArray();
+
+ _ = MemoryPatch.SetMemAccess(address, bytes.Length);
+ address.CopyFrom(bytes);
+ return;
+ }
+ NativePatches.Apply(patchName);
}
- NativePatches.Apply(patchName);
- }
}
diff --git a/managed/src/SwiftlyS2.Core/Modules/GameEvents/GameEventAccessor.cs b/managed/src/SwiftlyS2.Core/Modules/GameEvents/GameEventAccessor.cs
index c78173c7f..085e23a24 100644
--- a/managed/src/SwiftlyS2.Core/Modules/GameEvents/GameEventAccessor.cs
+++ b/managed/src/SwiftlyS2.Core/Modules/GameEvents/GameEventAccessor.cs
@@ -26,6 +26,7 @@ public void Dispose()
private void CheckIsValid()
{
if (!_IsValid) throw new InvalidOperationException("The event is already disposed.");
+ if (Address == 0) throw new InvalidOperationException("The event is invalid.");
}
public void SetBool( string key, bool value )
@@ -141,7 +142,7 @@ public CCSPlayerPawn GetPlayerPawn( string key )
CheckIsValid();
var playerid = GetInt32(key);
- return !NativePlayerManager.IsPlayerOnline(playerid) ? null : PlayerManagerService.PlayerObjects[playerid];
+ return PlayerManagerService.PlayerObjects.TryGetValue(playerid, out var player) ? player : null;
}
public void SetPtr( string key, nint value )
diff --git a/managed/src/SwiftlyS2.Core/Modules/GameEvents/GameEventCallback.cs b/managed/src/SwiftlyS2.Core/Modules/GameEvents/GameEventCallback.cs
index ec5351ccc..f34af9ff2 100644
--- a/managed/src/SwiftlyS2.Core/Modules/GameEvents/GameEventCallback.cs
+++ b/managed/src/SwiftlyS2.Core/Modules/GameEvents/GameEventCallback.cs
@@ -15,99 +15,97 @@ namespace SwiftlyS2.Core.GameEvents;
internal abstract class GameEventCallback : IEquatable, IDisposable
{
- public Guid Guid { get; init; }
+ public Guid Guid { get; init; }
- public string EventName { get; init; } = "";
+ public string EventName { get; init; } = "";
- public Type EventType { get; init; } = typeof(object);
+ public Type EventType { get; init; } = typeof(object);
- public bool IsPreHook { get; init; }
+ public bool IsPreHook { get; init; }
- public nint UnmanagedWrapperPtr { get; init; }
+ public nint UnmanagedWrapperPtr { get; init; }
- public ulong ListenerId { get; init; }
+ public ulong ListenerId { get; init; }
- public IContextedProfilerService Profiler { get; }
+ public IContextedProfilerService Profiler { get; }
- public ILoggerFactory LoggerFactory { get; }
+ public ILoggerFactory LoggerFactory { get; }
- public CoreContext Context { get; }
+ public CoreContext Context { get; }
- protected GameEventCallback( ILoggerFactory loggerFactory, IContextedProfilerService profiler, CoreContext context )
- {
- LoggerFactory = loggerFactory;
- Profiler = profiler;
- Context = context;
- }
+ protected GameEventCallback( ILoggerFactory loggerFactory, IContextedProfilerService profiler, CoreContext context )
+ {
+ LoggerFactory = loggerFactory;
+ Profiler = profiler;
+ Context = context;
+ }
+
+ public void Dispose()
+ {
+ if (IsPreHook)
+ {
+ NativeGameEvents.RemoveListenerPreCallback(ListenerId);
+ }
+ else
+ {
+ NativeGameEvents.RemoveListenerPostCallback(ListenerId);
+ }
+ }
- public void Dispose()
- {
- if (IsPreHook)
+ public bool Equals( GameEventCallback? other )
{
- NativeGameEvents.RemoveListenerPreCallback(ListenerId);
+ return other is not null && Guid == other.Guid;
}
- else
+
+ public override bool Equals( object? obj )
{
- NativeGameEvents.RemoveListenerPostCallback(ListenerId);
+ return ReferenceEquals(this, obj) || (obj is GameEventCallback other && Equals(other));
+ }
+
+ public override int GetHashCode()
+ {
+ return Guid.GetHashCode();
}
- }
-
- public bool Equals( GameEventCallback? other )
- {
- if (other is null) return false;
- return Guid == other.Guid;
- }
-
- public override bool Equals( object? obj )
- {
- if (ReferenceEquals(this, obj)) return true;
- return obj is GameEventCallback other && Equals(other);
- }
-
- public override int GetHashCode()
- {
- return Guid.GetHashCode();
- }
}
internal class GameEventCallback : GameEventCallback, IDisposable where T : IGameEvent
{
- private IGameEventService.GameEventHandler _callback { get; init; }
- private ILogger> _Logger { get; init; }
- private UnmanagedEventCallback _unmanagedCallback;
-
- public GameEventCallback( IGameEventService.GameEventHandler callback, bool pre, ILoggerFactory loggerFactory, IContextedProfilerService profiler, CoreContext context ) : base(loggerFactory, profiler, context)
- {
- Guid = Guid.NewGuid();
- EventType = typeof(T);
- IsPreHook = pre;
- EventName = T.GetName();
- _callback = callback;
- _Logger = LoggerFactory.CreateLogger>();
-
- _unmanagedCallback = ( hash, pEvent, pDontBroadcast ) =>
+ private IGameEventService.GameEventHandler _callback { get; init; }
+ private ILogger> _Logger { get; init; }
+ private UnmanagedEventCallback _unmanagedCallback;
+
+ public GameEventCallback( IGameEventService.GameEventHandler callback, bool pre, ILoggerFactory loggerFactory, IContextedProfilerService profiler, CoreContext context ) : base(loggerFactory, profiler, context)
{
- try
- {
- if (hash != T.GetHash()) return HookResult.Continue;
- var category = "GameEventCallback::" + EventName;
- Profiler.StartRecording(category);
- var eventObj = T.Create(pEvent);
- var result = _callback(eventObj);
- pDontBroadcast.Write(eventObj.DontBroadcast);
- eventObj.Dispose();
- Profiler.StopRecording(category);
- return result;
- }
- catch (Exception e)
- {
- if (!GlobalExceptionHandler.Handle(e)) return HookResult.Continue;
- _Logger.LogError(e, "Error in event {EventName} callback from context {ContextName}", EventName, Context.Name);
- return HookResult.Continue;
- }
- };
- UnmanagedWrapperPtr = Marshal.GetFunctionPointerForDelegate(_unmanagedCallback);
- NativeGameEvents.RegisterListener(EventName);
- ListenerId = IsPreHook ? NativeGameEvents.AddListenerPreCallback(UnmanagedWrapperPtr) : NativeGameEvents.AddListenerPostCallback(UnmanagedWrapperPtr);
- }
+ Guid = Guid.NewGuid();
+ EventType = typeof(T);
+ IsPreHook = pre;
+ EventName = T.GetName();
+ _callback = callback;
+ _Logger = LoggerFactory.CreateLogger>();
+
+ _unmanagedCallback = ( hash, pEvent, pDontBroadcast ) =>
+ {
+ try
+ {
+ if (hash != T.GetHash()) return HookResult.Continue;
+ var category = "GameEventCallback::" + EventName;
+ Profiler.StartRecording(category);
+ var eventObj = T.Create(pEvent);
+ var result = _callback(eventObj);
+ pDontBroadcast.Write(eventObj.DontBroadcast);
+ eventObj.Dispose();
+ Profiler.StopRecording(category);
+ return result;
+ }
+ catch (Exception e)
+ {
+ if (!GlobalExceptionHandler.Handle(e)) return HookResult.Continue;
+ _Logger.LogError(e, "Error in event {EventName} callback from context {ContextName}", EventName, Context.Name);
+ return HookResult.Continue;
+ }
+ };
+ UnmanagedWrapperPtr = Marshal.GetFunctionPointerForDelegate(_unmanagedCallback);
+ NativeGameEvents.RegisterListener(EventName);
+ ListenerId = IsPreHook ? NativeGameEvents.AddListenerPreCallback(UnmanagedWrapperPtr) : NativeGameEvents.AddListenerPostCallback(UnmanagedWrapperPtr);
+ }
}
diff --git a/managed/src/SwiftlyS2.Core/Modules/GameEvents/GameEventService.cs b/managed/src/SwiftlyS2.Core/Modules/GameEvents/GameEventService.cs
index 855548029..ac5786822 100644
--- a/managed/src/SwiftlyS2.Core/Modules/GameEvents/GameEventService.cs
+++ b/managed/src/SwiftlyS2.Core/Modules/GameEvents/GameEventService.cs
@@ -1,11 +1,9 @@
-using System.Reflection;
using Microsoft.Extensions.Logging;
using SwiftlyS2.Core.Natives;
+using SwiftlyS2.Core.Players;
using SwiftlyS2.Core.Scheduler;
using SwiftlyS2.Core.Services;
-using SwiftlyS2.Shared.GameEventDefinitions;
using SwiftlyS2.Shared.GameEvents;
-using SwiftlyS2.Shared.Misc;
using SwiftlyS2.Shared.Profiler;
namespace SwiftlyS2.Core.GameEvents;
@@ -23,8 +21,8 @@ public GameEventService( ILoggerFactory loggerFactory, CoreContext context, ICon
_Profiler = profiler;
}
- private readonly List _callbacks = new();
- private Lock _lock = new();
+ private readonly List _callbacks = [];
+ private readonly Lock _lock = new();
public Guid HookPre( IGameEventService.GameEventHandler callback ) where T : IGameEvent
{
@@ -52,7 +50,7 @@ public void Unhook( Guid guid )
{
lock (_lock)
{
- _callbacks.RemoveAll(callback =>
+ _ = _callbacks.RemoveAll(callback =>
{
if (callback.Guid == guid)
{
@@ -70,7 +68,7 @@ public void UnhookPre() where T : IGameEvent
{
lock (_lock)
{
- _callbacks.RemoveAll(callback =>
+ _ = _callbacks.RemoveAll(callback =>
{
if (callback.IsPreHook && callback is GameEventCallback)
{
@@ -87,7 +85,7 @@ public void UnhookPost() where T : IGameEvent
{
lock (_lock)
{
- _callbacks.RemoveAll(callback =>
+ _ = _callbacks.RemoveAll(callback =>
{
if (!callback.IsPreHook && callback is GameEventCallback)
{
@@ -103,9 +101,9 @@ public void UnhookPost() where T : IGameEvent
public void Fire() where T : IGameEvent
{
var handle = NativeGameEvents.CreateEvent(T.GetName());
- for (int i = 0; i < NativePlayerManager.GetPlayerCap(); i++)
+ for (var i = 0; i < NativePlayerManager.GetPlayerCap(); i++)
{
- if (NativeGameEvents.IsPlayerListeningToEventName(i, T.GetName()) && NativePlayerManager.IsPlayerOnline(i))
+ if (NativeGameEvents.IsPlayerListeningToEventName(i, T.GetName()) && PlayerManagerService.PlayerObjects.ContainsKey(i))
{
NativeGameEvents.FireEventToClient(handle, i);
}
@@ -120,9 +118,9 @@ public void Fire( Action configureEvent ) where T : IGameEvent
var eventObj = T.Create(handle);
configureEvent(eventObj);
eventObj.Dispose();
- for (int i = 0; i < NativePlayerManager.GetPlayerCap(); i++)
+ for (var i = 0; i < NativePlayerManager.GetPlayerCap(); i++)
{
- if (NativeGameEvents.IsPlayerListeningToEventName(i, T.GetName()) && NativePlayerManager.IsPlayerOnline(i))
+ if (NativeGameEvents.IsPlayerListeningToEventName(i, T.GetName()) && PlayerManagerService.PlayerObjects.ContainsKey(i))
{
NativeGameEvents.FireEventToClient(handle, i);
}
@@ -138,7 +136,7 @@ public Task FireAsync() where T : IGameEvent
public Task FireAsync( Action configureEvent ) where T : IGameEvent
{
- return SchedulerManager.QueueOrNow(() => Fire(configureEvent));
+ return SchedulerManager.QueueOrNow(() => Fire(configureEvent));
}
public void FireToPlayer( int slot ) where T : IGameEvent
diff --git a/managed/src/SwiftlyS2.Core/Modules/Helpers/Helpers.cs b/managed/src/SwiftlyS2.Core/Modules/Helpers/Helpers.cs
index af914bb75..764bd5451 100644
--- a/managed/src/SwiftlyS2.Core/Modules/Helpers/Helpers.cs
+++ b/managed/src/SwiftlyS2.Core/Modules/Helpers/Helpers.cs
@@ -92,12 +92,8 @@ internal class HelpersService : IHelpers
public CCSWeaponBaseVData? GetWeaponCSDataFromKey( int unknown, string key )
{
- nint weaponDataPtr = GameFunctions.GetWeaponCSDataFromKey(unknown, key);
- if (weaponDataPtr == 0)
- {
- return null;
- }
- return new CCSWeaponBaseVDataImpl(weaponDataPtr);
+ var weaponDataPtr = GameFunctions.GetWeaponCSDataFromKey(unknown, key);
+ return weaponDataPtr == 0 ? null : (CCSWeaponBaseVData)new CCSWeaponBaseVDataImpl(weaponDataPtr);
}
public CCSWeaponBaseVData? GetWeaponCSDataFromKey( int itemDefinitionIndex )
diff --git a/managed/src/SwiftlyS2.Core/Modules/Memory/MemoryService.cs b/managed/src/SwiftlyS2.Core/Modules/Memory/MemoryService.cs
index 9e5c9b0c7..7be8c28d6 100644
--- a/managed/src/SwiftlyS2.Core/Modules/Memory/MemoryService.cs
+++ b/managed/src/SwiftlyS2.Core/Modules/Memory/MemoryService.cs
@@ -1,4 +1,3 @@
-using System.Runtime.InteropServices;
using Microsoft.Extensions.Logging;
using SwiftlyS2.Core.Hooks;
using SwiftlyS2.Core.Natives;
@@ -14,8 +13,8 @@ internal class MemoryService : IMemoryService, IDisposable
private readonly ILogger _Logger;
private readonly HookManager _HookManager;
private readonly ILoggerFactory _LoggerFactory;
- private readonly Dictionary _UnmanagedFunctions = new();
- private readonly Dictionary _UnmanagedMemories = new();
+ private readonly Dictionary _UnmanagedFunctions = [];
+ private readonly Dictionary _UnmanagedMemories = [];
public MemoryService( ILogger logger, HookManager hookManager, ILoggerFactory loggerFactory )
{
@@ -110,15 +109,17 @@ public IUnmanagedMemory GetUnmanagedMemoryByAddress( nint address )
if (classes.Length == 1)
{
ptr = NativeMemoryHelpers.GetVirtualTableAddress(library, vtableName);
- }
+ }
else if (classes.Length == 2)
{
ptr = NativeMemoryHelpers.GetVirtualTableAddressNested2(library, classes[0], classes[1]);
}
- else {
+ else
+ {
throw new ArgumentException("Vtable has too many nested classes, which is not supported for now.");
}
- if (ptr == 0) {
+ if (ptr == 0)
+ {
ptr = null;
}
return ptr;
@@ -151,17 +152,17 @@ public T ToSchemaClass( nint address ) where T : class, ISchemaClass
return T.From(address);
}
- public nint Alloc(ulong size)
+ public nint Alloc( ulong size )
{
return NativeAllocator.Alloc(size);
}
- public void Free(nint pointer)
+ public void Free( nint pointer )
{
NativeAllocator.Free(pointer);
}
- public nint Resize(nint pointer, ulong newSize)
+ public nint Resize( nint pointer, ulong newSize )
{
return NativeAllocator.Resize(pointer, newSize);
}
diff --git a/managed/src/SwiftlyS2.Core/Modules/Memory/UnmanagedFunction.cs b/managed/src/SwiftlyS2.Core/Modules/Memory/UnmanagedFunction.cs
index 61b3ade63..850eb4b45 100644
--- a/managed/src/SwiftlyS2.Core/Modules/Memory/UnmanagedFunction.cs
+++ b/managed/src/SwiftlyS2.Core/Modules/Memory/UnmanagedFunction.cs
@@ -8,93 +8,86 @@ namespace SwiftlyS2.Core.Memory;
internal abstract class UnmanagedFunction : NativeHandle, IDisposable
{
+ public UnmanagedFunction( nint address ) : base(address)
+ {
+ }
- public UnmanagedFunction( nint address ) : base(address)
- {
- }
-
- public Type? DelegateType { get; init; }
+ public Type? DelegateType { get; init; }
- public abstract void Dispose();
+ public abstract void Dispose();
}
internal class UnmanagedFunction : UnmanagedFunction, IUnmanagedFunction, IDisposable where TDelegate : Delegate
{
- public new nint Address { get; private set; }
-
- public TDelegate CallOriginal {
- get {
- if (_HookManager.IsHooked(Address))
- {
- var original = _HookManager.GetOriginal(Address);
- if (original != nint.Zero)
- {
- return Marshal.GetDelegateForFunctionPointer(original);
+ public new nint Address { get; private set; }
+
+ public TDelegate CallOriginal {
+ get {
+ if (_HookManager.IsHooked(Address))
+ {
+ var original = _HookManager.GetOriginal(Address);
+ if (original != nint.Zero)
+ {
+ return Marshal.GetDelegateForFunctionPointer(original);
+ }
+ }
+ return Call;
}
- }
- return Call;
}
- }
-
- public TDelegate Call { get; private set; }
- public List Hooks { get; } = new();
+ public TDelegate Call { get; private set; }
- private HookManager _HookManager { get; set; }
+ public List Hooks { get; } = [];
- private ILogger> _Logger { get; set; }
+ private HookManager _HookManager { get; set; }
- public UnmanagedFunction( nint address, HookManager hookManager, ILoggerFactory loggerFactory ) : base(address)
- {
- _Logger = loggerFactory.CreateLogger>();
- _HookManager = hookManager;
- DelegateType = typeof(TDelegate);
+ private ILogger> _Logger { get; set; }
- Address = address;
+ public UnmanagedFunction( nint address, HookManager hookManager, ILoggerFactory loggerFactory ) : base(address)
+ {
+ _Logger = loggerFactory.CreateLogger>();
+ _HookManager = hookManager;
+ DelegateType = typeof(TDelegate);
- Call = Marshal.GetDelegateForFunctionPointer(address);
- }
+ Address = address;
- public Guid AddHook( Func, TDelegate> callbackBuilder )
- {
- try
- {
- var id = _HookManager.AddHook(Address, ( builder ) => callbackBuilder(() => Marshal.GetDelegateForFunctionPointer(builder())));
- Hooks.Add(id);
- return id;
+ Call = Marshal.GetDelegateForFunctionPointer(address);
}
- catch (Exception e)
+
+ public Guid AddHook( Func, TDelegate> callbackBuilder )
{
- if (!GlobalExceptionHandler.Handle(e)) return Guid.Empty;
- _Logger.LogError(e, "Failed to add hook to function {0}.", Address);
- return Guid.Empty;
+ try
+ {
+ var id = _HookManager.AddHook(Address, ( builder ) => callbackBuilder(() => Marshal.GetDelegateForFunctionPointer(builder())));
+ Hooks.Add(id);
+ return id;
+ }
+ catch (Exception e)
+ {
+ if (!GlobalExceptionHandler.Handle(e)) return Guid.Empty;
+ _Logger.LogError(e, "Failed to add hook to function {0}.", Address);
+ return Guid.Empty;
+ }
}
- }
- public void RemoveHook( Guid id )
- {
- try
+ public void RemoveHook( Guid id )
{
- _HookManager.Remove(new List { id });
- Hooks.Remove(id);
+ try
+ {
+ _HookManager.Remove([id]);
+ _ = Hooks.Remove(id);
+ }
+ catch (Exception e)
+ {
+ if (!GlobalExceptionHandler.Handle(e)) return;
+ _Logger.LogError(e, "Failed to remove hook {0} from function {1}.", id, Address);
+ }
}
- catch (Exception e)
+
+ public override void Dispose()
{
- if (!GlobalExceptionHandler.Handle(e)) return;
- _Logger.LogError(e, "Failed to remove hook {0} from function {1}.", id, Address);
+ _HookManager.Remove(Hooks);
+ Hooks.Clear();
}
- }
-
- public override void Dispose()
- {
- _HookManager.Remove(Hooks);
- Hooks.Clear();
- }
-
-
-
-
-
-
}
\ No newline at end of file
diff --git a/managed/src/SwiftlyS2.Core/Modules/Menus/MenuAPI.cs b/managed/src/SwiftlyS2.Core/Modules/Menus/MenuAPI.cs
index 142eee823..ba26e539a 100644
--- a/managed/src/SwiftlyS2.Core/Modules/Menus/MenuAPI.cs
+++ b/managed/src/SwiftlyS2.Core/Modules/Menus/MenuAPI.cs
@@ -44,17 +44,13 @@ internal sealed class MenuAPI : IMenuAPI, IDisposable
///
public IMenuBuilderAPI? Builder { get; init; }
- ///
- /// Gets or sets the default comment text to use when a menu option's Comment is not set.
- ///
- [Obsolete("Use Configuration.DefaultComment instead.")]
- public string DefaultComment { get; set; } = string.Empty;
-
///
/// Gets or sets an object that contains data about this menu.
///
public object? Tag { get; set; }
+ private bool WasFrozen { get; set; } = false;
+
///
/// The parent hierarchy information in a hierarchical menu structure.
///
@@ -290,9 +286,11 @@ private void OnRender()
{ }
}
- var playerStates = core.PlayerManager
- .GetAllPlayers()
- .Where(player => player.IsValid && !player.IsFakeClient)
+ try
+ {
+ var playerStates = core.PlayerManager
+ .GetAllValidPlayers()
+ .Where(player => !player.IsFakeClient)
.Select(player => (
Player: player,
DesiredIndex: desiredOptionIndex.TryGetValue(player.PlayerID, out var desired) ? desired : -1,
@@ -301,16 +299,18 @@ private void OnRender()
.Where(state => state.DesiredIndex >= 0 && state.SelectedIndex >= 0)
.ToList();
- var baseMaxVisibleItems = Configuration.MaxVisibleItems < 1 ? core.MenusAPI.Configuration.ItemsPerPage : Configuration.MaxVisibleItems;
- var maxVisibleItems = Configuration.AutoIncreaseVisibleItems
- ? Math.Clamp(baseMaxVisibleItems + (Configuration.HideTitle ? 1 : 0) + (Configuration.HideFooter ? 1 : 0), 1, 7)
- : Math.Clamp(baseMaxVisibleItems, 1, 5);
- var halfVisible = maxVisibleItems / 2;
+ var baseMaxVisibleItems = Configuration.MaxVisibleItems < 1 ? core.MenusAPI.Configuration.ItemsPerPage : Configuration.MaxVisibleItems;
+ var maxVisibleItems = Configuration.AutoIncreaseVisibleItems
+ ? Math.Clamp(baseMaxVisibleItems + (Configuration.HideTitle ? 1 : 0) + (Configuration.HideFooter ? 1 : 0), 1, 7)
+ : Math.Clamp(baseMaxVisibleItems, 1, 5);
+ var halfVisible = maxVisibleItems / 2;
- foreach (var (player, desiredIndex, selectedIndex) in playerStates)
- {
- ProcessPlayerMenu(player, desiredIndex, selectedIndex, maxOptions, maxVisibleItems, halfVisible);
+ foreach (var (player, desiredIndex, selectedIndex) in playerStates)
+ {
+ ProcessPlayerMenu(player, desiredIndex, selectedIndex, maxOptions, maxVisibleItems, halfVisible);
+ }
}
+ catch { }
}
private void ProcessPlayerMenu( IPlayer player, int desiredIndex, int selectedIndex, int maxOptions, int maxVisibleItems, int halfVisible )
@@ -459,9 +459,9 @@ private string BuildMenuHtml( IPlayer player, IReadOnlyList visible
"
",
guideLine,
"
",
- Configuration.HideComment
+ Configuration.HideComment || string.IsNullOrWhiteSpace(Configuration.DefaultComment)
? string.Empty
- : string.IsNullOrWhiteSpace(Configuration.DefaultComment) ? $"\u00A0\u00A0\u00A0
" : $"{Configuration.DefaultComment}
"
+ : $"{Configuration.DefaultComment}
"
);
var claimInfo = optionBase?.InputClaimInfo ?? MenuInputClaimInfo.Empty;
@@ -602,6 +602,7 @@ public void HideForPlayer( IPlayer player )
{
NativePlayer.ClearCenterMenuRender(player.PlayerID);
core.Scheduler.NextTick(() => NativePlayer.ClearCenterMenuRender(player.PlayerID));
+ _ = core.Scheduler.Delay(5, () => NativePlayer.ClearCenterMenuRender(player.PlayerID));
// Remove viewer, pause animations if no viewers left
lock (viewersLock)
@@ -765,10 +766,22 @@ private void SetFreezeState( IPlayer player, bool freeze )
core.Scheduler.NextTick(() =>
{
- var moveType = freeze ? MoveType_t.MOVETYPE_NONE : MoveType_t.MOVETYPE_WALK;
- player.PlayerPawn.MoveType = moveType;
- player.PlayerPawn.ActualMoveType = moveType;
- player.PlayerPawn.MoveTypeUpdated();
+ if (freeze)
+ {
+ var moveType = MoveType_t.MOVETYPE_NONE;
+ player.PlayerPawn.MoveType = moveType;
+ player.PlayerPawn.ActualMoveType = moveType;
+ player.PlayerPawn.MoveTypeUpdated();
+ WasFrozen = true;
+ }
+ else if (WasFrozen && !freeze)
+ {
+ var moveType = MoveType_t.MOVETYPE_WALK;
+ player.PlayerPawn.MoveType = moveType;
+ player.PlayerPawn.ActualMoveType = moveType;
+ player.PlayerPawn.MoveTypeUpdated();
+ WasFrozen = false;
+ }
});
}
diff --git a/managed/src/SwiftlyS2.Core/Modules/Menus/OptionsBase/MenuOptionBase.cs b/managed/src/SwiftlyS2.Core/Modules/Menus/OptionsBase/MenuOptionBase.cs
index 708f5ea39..8a2780e51 100644
--- a/managed/src/SwiftlyS2.Core/Modules/Menus/OptionsBase/MenuOptionBase.cs
+++ b/managed/src/SwiftlyS2.Core/Modules/Menus/OptionsBase/MenuOptionBase.cs
@@ -29,6 +29,7 @@ public abstract partial class MenuOptionBase : IMenuOption, IDisposable
protected MenuOptionBase()
{
disposed = false;
+ Comment = "";
}
///
@@ -44,16 +45,17 @@ protected MenuOptionBase()
protected MenuOptionBase( int updateIntervalMs, int pauseIntervalMs )
{
disposed = false;
+ Comment = "";
if (updateIntervalMs < (int)(1 / 64f * 1000))
{
- Spectre.Console.AnsiConsole.WriteException(new ArgumentOutOfRangeException(nameof(updateIntervalMs),
+ AnsiConsole.WriteException(new ArgumentOutOfRangeException(nameof(updateIntervalMs),
$"updateIntervalMs: value {updateIntervalMs} is out of range."));
}
if (pauseIntervalMs < (int)(1 / 64f * 1000))
{
- Spectre.Console.AnsiConsole.WriteException(new ArgumentOutOfRangeException(nameof(pauseIntervalMs),
+ AnsiConsole.WriteException(new ArgumentOutOfRangeException(nameof(pauseIntervalMs),
$"pauseIntervalMs: value {pauseIntervalMs} is out of range."));
}
@@ -241,7 +243,7 @@ public float MaxWidth {
if (value < 1f)
{
- Spectre.Console.AnsiConsole.WriteException(new ArgumentOutOfRangeException(nameof(MaxWidth),
+ AnsiConsole.WriteException(new ArgumentOutOfRangeException(nameof(MaxWidth),
$"MaxWidth: value {value:F3} is out of range."));
}
diff --git a/managed/src/SwiftlyS2.Core/Modules/NetMessages/NetMessageService.cs b/managed/src/SwiftlyS2.Core/Modules/NetMessages/NetMessageService.cs
index 4a244dd21..0933d0fc8 100644
--- a/managed/src/SwiftlyS2.Core/Modules/NetMessages/NetMessageService.cs
+++ b/managed/src/SwiftlyS2.Core/Modules/NetMessages/NetMessageService.cs
@@ -1,6 +1,5 @@
using Microsoft.Extensions.Logging;
using SwiftlyS2.Core.Natives;
-using SwiftlyS2.Shared.Natives;
using SwiftlyS2.Shared.NetMessages;
using SwiftlyS2.Shared.Profiler;
@@ -10,7 +9,7 @@ namespace SwiftlyS2.Core.NetMessages;
internal class NetMessageService : INetMessageService, IDisposable
{
- private List _callbacks = new();
+ private List _callbacks = [];
private ILoggerFactory _loggerFactory;
private IContextedProfilerService _profiler;
private Lock _lock = new();
@@ -56,7 +55,7 @@ public void Unhook( Guid guid )
{
lock (_lock)
{
- _callbacks.RemoveAll(callback =>
+ _ = _callbacks.RemoveAll(callback =>
{
if (callback.Guid == guid)
{
@@ -72,7 +71,7 @@ public void UnhookClientMessage() where T : ITypedProtobuf, INetMessage
{
lock (_lock)
{
- _callbacks.RemoveAll(callback =>
+ _ = _callbacks.RemoveAll(callback =>
{
if (callback is NetMessageClientHookCallback clientHook)
{
@@ -88,7 +87,7 @@ public void UnhookServerMessage() where T : ITypedProtobuf, INetMessage
{
lock (_lock)
{
- _callbacks.RemoveAll(callback =>
+ _ = _callbacks.RemoveAll(callback =>
{
if (callback is NetMessageServerHookCallback serverHook)
{
@@ -104,7 +103,7 @@ public void UnhookServerMessageInternal() where T : ITypedProtobuf, INetMe
{
lock (_lock)
{
- _callbacks.RemoveAll(callback =>
+ _ = _callbacks.RemoveAll(callback =>
{
if (callback is NetMessageServerInternalHookCallback serverInternalHook)
{
@@ -119,8 +118,9 @@ public void UnhookServerMessageInternal() where T : ITypedProtobuf, INetMe
private nint AllocateNetMessage( int msgId )
{
var handle = NativeNetMessages.AllocateNetMessageByID(msgId);
- if (handle == 0) throw new InvalidOperationException("Failed to allocate net message. This is possibly caused by the message ID is already deprecated not supported in game.");
- return handle;
+ return handle == 0
+ ? throw new InvalidOperationException("Failed to allocate net message. This is possibly caused by the message ID is already deprecated not supported in game.")
+ : handle;
}
public T Create() where T : ITypedProtobuf, INetMessage, IDisposable
diff --git a/managed/src/SwiftlyS2.Core/Modules/Permissions/PermissionManager.cs b/managed/src/SwiftlyS2.Core/Modules/Permissions/PermissionManager.cs
index 1bc77d9a9..85569c1fe 100644
--- a/managed/src/SwiftlyS2.Core/Modules/Permissions/PermissionManager.cs
+++ b/managed/src/SwiftlyS2.Core/Modules/Permissions/PermissionManager.cs
@@ -1,4 +1,3 @@
-using System.Collections.Concurrent;
using System.Collections.Immutable;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ -11,267 +10,267 @@ namespace SwiftlyS2.Core.Permissions;
internal class PermissionManager : IPermissionManager
{
- private Dictionary> _playerPermissions = new();
- private Dictionary> _temporaryPlayerPermissions = new();
- private Dictionary> _subPermissions = new();
- private Dictionary> _temporarySubPermissions = new();
- private List _defaultPermissions = new();
- private ImmutableDictionary _queryCache = ImmutableDictionary.Create();
- private object _lock = new();
+ private Dictionary> _playerPermissions = [];
+ private Dictionary> _temporaryPlayerPermissions = [];
+ private Dictionary> _subPermissions = [];
+ private Dictionary> _temporarySubPermissions = [];
+ private List _defaultPermissions = [];
+ private ImmutableDictionary _queryCache = ImmutableDictionary.Create();
+ private object _lock = new();
- public PermissionManager( IOptionsMonitor options, ILogger logger )
- {
- LoadPermissions(options.CurrentValue);
-
- options.OnChange(( config ) =>
- {
- try
- {
- logger.LogInformation("Permission config changed, reloading...");
- LoadPermissions(config);
- logger.LogInformation("Permission config reloaded.");
- }
- catch (Exception e)
- {
- if (!GlobalExceptionHandler.Handle(e)) return;
- logger.LogError(e, "Error reloading permission config.");
- }
- });
- }
-
- private void LoadPermissions( PermissionConfig config )
- {
- lock (_lock)
+ public PermissionManager( IOptionsMonitor options, ILogger logger )
{
- _queryCache = _queryCache.Clear();
- _defaultPermissions = config.PermissionGroups.ContainsKey("__default") ? config.PermissionGroups["__default"] : [];
- _playerPermissions = config.Players.ToDictionary(x => ulong.Parse(x.Key), x => x.Value);
- _subPermissions = config.PermissionGroups;
- }
- }
+ LoadPermissions(options.CurrentValue);
- private List GetPlayerPermissions( ulong playerId )
- {
- List permissions = new();
+ _ = options.OnChange(( config ) =>
+ {
+ try
+ {
+ logger.LogInformation("Permission config changed, reloading...");
+ LoadPermissions(config);
+ logger.LogInformation("Permission config reloaded.");
+ }
+ catch (Exception e)
+ {
+ if (!GlobalExceptionHandler.Handle(e)) return;
+ logger.LogError(e, "Error reloading permission config.");
+ }
+ });
+ }
- if (_playerPermissions.TryGetValue(playerId, out var playerPermission))
+ private void LoadPermissions( PermissionConfig config )
{
- permissions.AddRange(playerPermission);
+ lock (_lock)
+ {
+ _queryCache = _queryCache.Clear();
+ _defaultPermissions = config.PermissionGroups.ContainsKey("__default") ? config.PermissionGroups["__default"] : [];
+ _playerPermissions = config.Players.ToDictionary(x => ulong.Parse(x.Key), x => x.Value);
+ _subPermissions = config.PermissionGroups;
+ }
}
- if (_temporaryPlayerPermissions.TryGetValue(playerId, out var temporaryPermissions))
+ private List GetPlayerPermissions( ulong playerId )
{
- permissions.AddRange(temporaryPermissions);
- }
+ List permissions = [];
- permissions.AddRange(_defaultPermissions);
+ if (_playerPermissions.TryGetValue(playerId, out var playerPermission))
+ {
+ permissions.AddRange(playerPermission);
+ }
- return permissions;
- }
+ if (_temporaryPlayerPermissions.TryGetValue(playerId, out var temporaryPermissions))
+ {
+ permissions.AddRange(temporaryPermissions);
+ }
- private Dictionary> GetSubPermissions()
- {
- var result = new Dictionary>();
+ permissions.AddRange(_defaultPermissions);
- foreach (var kvp in _subPermissions)
- {
- result[kvp.Key] = [.. kvp.Value];
+ return permissions;
}
- foreach (var kvp in _temporarySubPermissions)
+ private Dictionary> GetSubPermissions()
{
- if (result.TryGetValue(kvp.Key, out var existingPermissions))
- {
- var permissionSet = new HashSet(existingPermissions);
- foreach (var perm in kvp.Value)
+ var result = new Dictionary>();
+
+ foreach (var kvp in _subPermissions)
{
- permissionSet.Add(perm);
+ result[kvp.Key] = [.. kvp.Value];
}
- result[kvp.Key] = permissionSet.ToList();
- }
- else
- {
- result[kvp.Key] = [.. kvp.Value];
- }
- }
- return result;
- }
+ foreach (var kvp in _temporarySubPermissions)
+ {
+ if (result.TryGetValue(kvp.Key, out var existingPermissions))
+ {
+ var permissionSet = new HashSet(existingPermissions);
+ foreach (var perm in kvp.Value)
+ {
+ _ = permissionSet.Add(perm);
+ }
+ result[kvp.Key] = permissionSet.ToList();
+ }
+ else
+ {
+ result[kvp.Key] = [.. kvp.Value];
+ }
+ }
- private bool IsEqual( string from, string target )
- {
- if (from == "*")
- {
- return true;
+ return result;
}
- if (!from.Contains("*"))
- {
- return string.Equals(from, target, StringComparison.OrdinalIgnoreCase);
- }
-
- var prefix = from[..^2];
- return target.StartsWith(prefix, StringComparison.OrdinalIgnoreCase);
- }
- private bool HasNestedPermission( string rootPermission, string targetPermission, HashSet visitedPermissions )
- {
- if (visitedPermissions.Contains(rootPermission))
+ private bool IsEqual( string from, string target )
{
- AnsiConsole.WriteLine("Loop detected for permission: " + rootPermission);
- return false;
- }
-
- visitedPermissions.Add(rootPermission);
+ if (from == "*")
+ {
+ return true;
+ }
+ if (!from.Contains("*"))
+ {
+ return string.Equals(from, target, StringComparison.OrdinalIgnoreCase);
+ }
- if (IsEqual(rootPermission, targetPermission))
- {
- return true;
+ var prefix = from[..^2];
+ return target.StartsWith(prefix, StringComparison.OrdinalIgnoreCase);
}
- if (GetSubPermissions().TryGetValue(rootPermission, out var subPermissions))
+ private bool HasNestedPermission( string rootPermission, string targetPermission, HashSet visitedPermissions )
{
- foreach (var subPermission in subPermissions)
- {
- if (HasNestedPermission(subPermission, targetPermission, visitedPermissions))
+ if (visitedPermissions.Contains(rootPermission))
{
- return true;
+ AnsiConsole.WriteLine("Loop detected for permission: " + rootPermission);
+ return false;
}
- }
- }
- return false;
- }
+ _ = visitedPermissions.Add(rootPermission);
- public bool PlayerHasPermissions( ulong playerId, IEnumerable permissions )
- {
- foreach (var permission in permissions)
- {
- if (!PlayerHasPermission(playerId, permission))
- {
- return false;
- }
- }
+ if (IsEqual(rootPermission, targetPermission))
+ {
+ return true;
+ }
- return true;
- }
+ if (GetSubPermissions().TryGetValue(rootPermission, out var subPermissions))
+ {
+ foreach (var subPermission in subPermissions)
+ {
+ if (HasNestedPermission(subPermission, targetPermission, visitedPermissions))
+ {
+ return true;
+ }
+ }
+ }
- public bool PlayerHasPermission( ulong playerId, string permission )
- {
- var key = new PermissionCacheKey { PlayerId = playerId, Permission = permission };
- if (_queryCache.TryGetValue(key, out var result))
- {
- return result;
+ return false;
}
- lock (_lock)
+ public bool PlayerHasPermissions( ulong playerId, IEnumerable permissions )
{
- var permissions = GetPlayerPermissions(playerId);
-
- if (permissions.Count == 0)
- {
- _queryCache = _queryCache.Add(key, false);
- return false;
- }
+ foreach (var permission in permissions)
+ {
+ if (!PlayerHasPermission(playerId, permission))
+ {
+ return false;
+ }
+ }
- if (permissions.Any(p => IsEqual(p, permission)))
- {
- _queryCache = _queryCache.Add(key, true);
return true;
- }
+ }
- foreach (var perm in permissions)
- {
- if (HasNestedPermission(perm, permission, new HashSet()))
+ public bool PlayerHasPermission( ulong playerId, string permission )
+ {
+ var key = new PermissionCacheKey { PlayerId = playerId, Permission = permission };
+ if (_queryCache.TryGetValue(key, out var result))
{
- _queryCache = _queryCache.Add(key, true);
- return true;
+ return result;
}
- }
- _queryCache = _queryCache.Add(key, false);
- return false;
- }
+ lock (_lock)
+ {
+ var permissions = GetPlayerPermissions(playerId);
+
+ if (permissions.Count == 0)
+ {
+ _queryCache = _queryCache.Add(key, false);
+ return false;
+ }
+
+ if (permissions.Any(p => IsEqual(p, permission)))
+ {
+ _queryCache = _queryCache.Add(key, true);
+ return true;
+ }
+
+ foreach (var perm in permissions)
+ {
+ if (HasNestedPermission(perm, permission, []))
+ {
+ _queryCache = _queryCache.Add(key, true);
+ return true;
+ }
+ }
+
+ _queryCache = _queryCache.Add(key, false);
+ return false;
+ }
- }
+ }
- public void AddPermission( ulong playerId, string permission )
- {
- lock (_lock)
+ public void AddPermission( ulong playerId, string permission )
{
- if (_temporaryPlayerPermissions.TryGetValue(playerId, out var permissions))
- {
- if (!permissions.Contains(permission))
+ lock (_lock)
{
- permissions.Add(permission);
+ if (_temporaryPlayerPermissions.TryGetValue(playerId, out var permissions))
+ {
+ if (!permissions.Contains(permission))
+ {
+ permissions.Add(permission);
+ }
+ }
+ else
+ {
+ _temporaryPlayerPermissions[playerId] = [permission];
+ }
+
+ _queryCache = _queryCache.Clear();
}
- }
- else
- {
- _temporaryPlayerPermissions[playerId] = [permission];
- }
-
- _queryCache = _queryCache.Clear();
}
- }
- public void RemovePermission( ulong playerId, string permission )
- {
- lock (_lock)
+ public void RemovePermission( ulong playerId, string permission )
{
- if (_temporaryPlayerPermissions.TryGetValue(playerId, out var permissions))
- {
- if (permissions.Contains(permission))
+ lock (_lock)
{
- permissions.Remove(permission);
+ if (_temporaryPlayerPermissions.TryGetValue(playerId, out var permissions))
+ {
+ if (permissions.Contains(permission))
+ {
+ _ = permissions.Remove(permission);
+ }
+ }
}
- }
- }
- _queryCache = _queryCache.Clear();
- }
+ _queryCache = _queryCache.Clear();
+ }
- public void AddSubPermission( string permission, string subPermission )
- {
- lock (_lock)
+ public void AddSubPermission( string permission, string subPermission )
{
- if (_temporarySubPermissions.TryGetValue(permission, out var subPermissions))
- {
- if (!subPermissions.Contains(subPermission))
+ lock (_lock)
{
- subPermissions.Add(subPermission);
+ if (_temporarySubPermissions.TryGetValue(permission, out var subPermissions))
+ {
+ if (!subPermissions.Contains(subPermission))
+ {
+ subPermissions.Add(subPermission);
+ }
+ }
+ else
+ {
+ _temporarySubPermissions[permission] = [subPermission];
+ }
}
- }
- else
- {
- _temporarySubPermissions[permission] = [subPermission];
- }
+ _queryCache = _queryCache.Clear();
}
- _queryCache = _queryCache.Clear();
- }
- public void RemoveSubPermission( string permission, string subPermission )
- {
- lock (_lock)
+ public void RemoveSubPermission( string permission, string subPermission )
{
- if (_temporarySubPermissions.TryGetValue(permission, out var subPermissions))
- {
- if (subPermissions.Contains(subPermission))
+ lock (_lock)
{
- subPermissions.Remove(subPermission);
+ if (_temporarySubPermissions.TryGetValue(permission, out var subPermissions))
+ {
+ if (subPermissions.Contains(subPermission))
+ {
+ _ = subPermissions.Remove(subPermission);
+ }
+ }
}
- }
- }
- _queryCache = _queryCache.Clear();
- }
+ _queryCache = _queryCache.Clear();
+ }
- public void ClearPermission( ulong playerId )
- {
- lock (_lock)
+ public void ClearPermission( ulong playerId )
{
- _temporaryPlayerPermissions.Remove(playerId);
- }
+ lock (_lock)
+ {
+ _ = _temporaryPlayerPermissions.Remove(playerId);
+ }
- _queryCache = _queryCache.Clear();
- }
+ _queryCache = _queryCache.Clear();
+ }
}
diff --git a/managed/src/SwiftlyS2.Core/Modules/Players/Player.cs b/managed/src/SwiftlyS2.Core/Modules/Players/Player.cs
index db2bc2f93..5f454ada0 100644
--- a/managed/src/SwiftlyS2.Core/Modules/Players/Player.cs
+++ b/managed/src/SwiftlyS2.Core/Modules/Players/Player.cs
@@ -10,33 +10,42 @@
namespace SwiftlyS2.Core.Players;
-internal class Player : IPlayer
+internal class Player : IPlayer, IDisposable
{
public Player( int pid )
{
Slot = pid;
+ SessionId = NativePlayer.GetSessionID(pid);
}
+ ~Player()
+ {
+ Dispose();
+ }
+
+ private bool _disposed = false;
+
public int PlayerID => Slot;
+ public ulong SessionId { get; }
public int Slot { get; }
- public int UserID => NativePlayer.GetUserID(Slot);
+ public int UserID { get { ThrowIfDisposed(); return NativePlayer.GetUserID(Slot); } }
- public bool IsFakeClient => NativePlayer.IsFakeClient(Slot);
+ public string Name { get { ThrowIfDisposed(); return NativePlayer.GetName(Slot); } }
- public bool IsAuthorized => NativePlayer.IsAuthorized(Slot);
+ public bool IsFakeClient { get { ThrowIfDisposed(); return NativePlayer.IsFakeClient(Slot); } }
- public uint ConnectedTime => NativePlayer.GetConnectedTime(Slot);
+ public bool IsAuthorized { get { ThrowIfDisposed(); return NativePlayer.IsAuthorized(Slot); } }
+ public uint ConnectedTime { get { ThrowIfDisposed(); return NativePlayer.GetConnectedTime(Slot); } }
- public Language PlayerLanguage => new(NativePlayer.GetLanguage(Slot));
+ public Language PlayerLanguage { get { ThrowIfDisposed(); return new(NativePlayer.GetLanguage(Slot)); } }
- public ulong SteamID => NativePlayer.GetSteamID(Slot);
+ public ulong SteamID { get { ThrowIfDisposed(); return NativePlayer.GetSteamID(Slot); } }
- public ulong UnauthorizedSteamID => NativePlayer.GetUnauthorizedSteamID(Slot);
-
- public CCSPlayerController Controller => new CCSPlayerControllerImpl(NativePlayer.GetController(Slot));
+ public ulong UnauthorizedSteamID { get { ThrowIfDisposed(); return NativePlayer.GetUnauthorizedSteamID(Slot); } }
+ public CCSPlayerController Controller { get { ThrowIfDisposed(); return new CCSPlayerControllerImpl(NativePlayer.GetController(Slot)); } }
public CCSPlayerController RequiredController => Controller is { IsValid: true } controller ? controller : throw new InvalidOperationException("Controller is not valid");
public CBasePlayerPawn? Pawn => Controller?.Pawn.Value;
@@ -47,59 +56,73 @@ public Player( int pid )
public CCSPlayerPawn RequiredPlayerPawn => PlayerPawn is { IsValid: true } pawn ? pawn : throw new InvalidOperationException("PlayerPawn is not valid");
- public GameButtonFlags PressedButtons => (GameButtonFlags)NativePlayer.GetPressedButtons(Slot);
-
- public string IPAddress => NativePlayer.GetIPAddress(Slot);
+ public GameButtonFlags PressedButtons { get { ThrowIfDisposed(); return (GameButtonFlags)NativePlayer.GetPressedButtons(Slot); } }
- public VoiceFlagValue VoiceFlags { get => (VoiceFlagValue)NativeVoiceManager.GetClientVoiceFlags(Slot); set => NativeVoiceManager.SetClientVoiceFlags(Slot, (int)value); }
+ public string IPAddress { get { ThrowIfDisposed(); return NativePlayer.GetIPAddress(Slot); } }
+ public VoiceFlagValue VoiceFlags { get { ThrowIfDisposed(); return (VoiceFlagValue)NativeVoiceManager.GetClientVoiceFlags(Slot); } set { ThrowIfDisposed(); NativeVoiceManager.SetClientVoiceFlags(Slot, (int)value); } }
public bool IsValid =>
+ !_disposed &&
Controller is { IsValid: true, IsHLTV: false, Connected: PlayerConnectedState.PlayerConnected } &&
Pawn is { IsValid: true };
public bool IsAlive => IsValid && Pawn!.LifeState == (byte)LifeState_t.LIFE_ALIVE;
- public bool IsFirstSpawn => NativePlayer.IsFirstSpawn(Slot);
+ public bool IsFirstSpawn { get { ThrowIfDisposed(); return NativePlayer.IsFirstSpawn(Slot); } }
Language IPlayer.PlayerLanguage => PlayerLanguage;
public void ChangeTeam( Team team )
{
+ ThrowIfDisposed();
NativePlayer.ChangeTeam(Slot, (byte)team);
}
public Task ChangeTeamAsync( Team team )
{
+ ThrowIfDisposed();
return SchedulerManager.QueueOrNow(() => ChangeTeam(team));
}
public void ClearTransmitEntityBlocks()
{
+ ThrowIfDisposed();
NativePlayer.ClearTransmitEntityBlocked(Slot);
}
public ListenOverride GetListenOverride( int player )
{
+ ThrowIfDisposed();
return (ListenOverride)NativeVoiceManager.GetClientListenOverride(Slot, player);
}
public bool IsTransmitEntityBlocked( int entityid )
{
+ ThrowIfDisposed();
return NativePlayer.IsTransmitEntityBlocked(Slot, entityid);
}
public void Kick( string reason, ENetworkDisconnectionReason gameReason )
{
- NativePlayer.Kick(Slot, reason, (int)gameReason);
+ ThrowIfDisposed();
+ CancellationTokenSource cts = new();
+ SchedulerManager.NextTick(() =>
+ {
+ NativePlayer.Kick(Slot, reason, (int)gameReason);
+ }, cts.Token);
}
public Task KickAsync( string reason, ENetworkDisconnectionReason gameReason )
{
- return SchedulerManager.QueueOrNow(() => Kick(reason, gameReason));
+ return SchedulerManager.NextTickAsync(() =>
+ {
+ NativePlayer.Kick(Slot, reason, (int)gameReason);
+ });
}
public void SendMessage( MessageType kind, string message )
{
+ ThrowIfDisposed();
NativePlayer.SendMessage(Slot, (int)kind, message, 5000);
}
@@ -110,16 +133,19 @@ public Task SendMessageAsync( MessageType kind, string message )
public void SetListenOverride( int player, ListenOverride listenOverride )
{
+ ThrowIfDisposed();
NativeVoiceManager.SetClientListenOverride(Slot, player, (int)listenOverride);
}
public void ShouldBlockTransmitEntity( int entityid, bool shouldBlockTransmit )
{
+ ThrowIfDisposed();
NativePlayer.ShouldBlockTransmitEntity(Slot, entityid, shouldBlockTransmit);
}
public void SwitchTeam( Team team )
{
+ ThrowIfDisposed();
NativePlayer.SwitchTeam(Slot, (byte)team);
}
@@ -130,6 +156,7 @@ public Task SwitchTeamAsync( Team team )
public void TakeDamage( CTakeDamageInfo damageInfo )
{
+ ThrowIfDisposed();
unsafe
{
NativePlayer.TakeDamage(Slot, (nint)(&damageInfo));
@@ -179,6 +206,7 @@ public void Respawn()
public void ExecuteCommand( string command )
{
+ ThrowIfDisposed();
NativePlayer.ExecuteCommand(Slot, command);
}
@@ -189,7 +217,7 @@ public Task ExecuteCommandAsync( string command )
public bool Equals( IPlayer? other )
{
- return other is not null && PlayerID == other.PlayerID;
+ return other is not null && SessionId == other.SessionId;
}
public override bool Equals( object? obj )
@@ -204,6 +232,7 @@ public override int GetHashCode()
public void SendMessage( MessageType kind, string message, int htmlDuration = 5000 )
{
+ ThrowIfDisposed();
NativePlayer.SendMessage(Slot, (int)kind, message, htmlDuration);
}
@@ -282,6 +311,19 @@ public Task SendChatEOTAsync( string message )
return SendMessageAsync(MessageType.ChatEOT, message);
}
+ public void Dispose()
+ {
+ _disposed = true;
+ }
+
+ public void ThrowIfDisposed()
+ {
+ if (_disposed)
+ {
+ throw new ObjectDisposedException($"Player object (slot={Slot},sessionId={SessionId}) has been disposed.");
+ }
+ }
+
public static bool operator ==( Player? left, Player? right )
{
return left is not null && right is not null && left.Equals(right);
diff --git a/managed/src/SwiftlyS2.Core/Modules/Players/PlayerManagerService.cs b/managed/src/SwiftlyS2.Core/Modules/Players/PlayerManagerService.cs
index ea92f1d55..2829d1dd9 100644
--- a/managed/src/SwiftlyS2.Core/Modules/Players/PlayerManagerService.cs
+++ b/managed/src/SwiftlyS2.Core/Modules/Players/PlayerManagerService.cs
@@ -1,7 +1,7 @@
-using SwiftlyS2.Core.Natives;
+using System.Collections.Concurrent;
+using SwiftlyS2.Core.Natives;
using SwiftlyS2.Core.Scheduler;
using SwiftlyS2.Core.SchemaDefinitions;
-using SwiftlyS2.Shared;
using SwiftlyS2.Shared.Players;
using SwiftlyS2.Shared.SchemaDefinitions;
using SwiftlyS2.Shared.SteamAPI;
@@ -11,14 +11,34 @@ namespace SwiftlyS2.Core.Players;
internal class PlayerManagerService : IPlayerManagerService
{
- private ITranslationService _translationService;
- public static List PlayerObjects = Enumerable.Range(0, NativePlayerManager.GetPlayerCap()).Select(i => new Player(i) as IPlayer).ToList();
+ private readonly ITranslationService _translationService;
+ public static ConcurrentDictionary PlayerObjects { get; } = new();
+ public static ConcurrentDictionary SessionIdToPlayerObjects { get; } = new();
public PlayerManagerService( ITranslationService translationService )
{
_translationService = translationService;
}
+ public static void RegisterPlayerObject( int playerid )
+ {
+ UnregisterPlayerObject(playerid);
+ var player = new Player(playerid);
+ _ = PlayerObjects.TryAdd(playerid, player);
+ _ = SessionIdToPlayerObjects.TryAdd(player.SessionId, player);
+ }
+
+ public static void UnregisterPlayerObject( int playerid )
+ {
+ if (PlayerObjects.TryGetValue(playerid, out var player))
+ {
+ var p = (Player)player;
+ _ = SessionIdToPlayerObjects.TryRemove(p.SessionId, out var _);
+ p.Dispose();
+ _ = PlayerObjects.TryRemove(playerid, out var _);
+ }
+ }
+
public int PlayerCount => NativePlayerManager.GetPlayerCount();
public int PlayerCap => NativePlayerManager.GetPlayerCap();
@@ -30,7 +50,7 @@ public void ClearAllBlockedTransmitEntities()
public IPlayer? GetPlayer( int playerid )
{
- return !IsPlayerOnline(playerid) ? null : PlayerObjects[playerid];
+ return PlayerObjects.TryGetValue(playerid, out var player) ? player : null;
}
public IPlayer? GetPlayerFromController( CBasePlayerController controller )
@@ -45,7 +65,7 @@ public void ClearAllBlockedTransmitEntities()
public bool IsPlayerOnline( int playerid )
{
- return NativePlayerManager.IsPlayerOnline(playerid);
+ return PlayerObjects.ContainsKey(playerid);
}
public void SendMessage( MessageType kind, string message )
@@ -65,8 +85,7 @@ public void ShouldBlockTransmitEntity( int entityid, bool shouldBlockTransmit )
public IEnumerable GetAllPlayers()
{
- return PlayerObjects
- .Where(( p ) => IsPlayerOnline(p.PlayerID));
+ return PlayerObjects.Values;
}
public IEnumerable GetAllValidPlayers()
@@ -79,7 +98,7 @@ public IEnumerable FindTargettedPlayers( IPlayer player, string target,
{
IEnumerable allPlayers = [];
- var players = GetAllPlayers();
+ var players = GetAllValidPlayers();
foreach (var targetPlayer in players)
{
if (searchMode.HasFlag(TargetSearchMode.NoBots) && targetPlayer.IsFakeClient)
@@ -159,7 +178,7 @@ public IEnumerable FindTargettedPlayers( IPlayer player, string target,
}
else if (target.StartsWith('#'))
{
- if (int.TryParse(target[1..], out int id) && targetPlayer.PlayerID == id)
+ if (int.TryParse(target[1..], out var id) && targetPlayer.PlayerID == id)
{
allPlayers = allPlayers.Append(targetPlayer);
}
@@ -329,4 +348,31 @@ public Task SendMessageAsync( MessageType kind, Func SendMessage(kind, messageCallback, htmlDuration));
}
+
+ public IPlayer? GetPlayerFromSteamId( ulong steamId, bool allowUnauthorized = true )
+ {
+ return PlayerObjects.Values.FirstOrDefault(p =>
+ {
+ if (allowUnauthorized)
+ {
+ if (p.SteamID == steamId) return true;
+ if (p.UnauthorizedSteamID == steamId) return true;
+ }
+ else
+ {
+ if (p.SteamID == steamId && p.IsAuthorized) return true;
+ }
+ return false;
+ });
+ }
+
+ public bool IsSessionIdValid( ulong sessionId )
+ {
+ return SessionIdToPlayerObjects.ContainsKey(sessionId);
+ }
+
+ public IPlayer? GetPlayerFromSessionId( ulong sessionId )
+ {
+ return SessionIdToPlayerObjects.TryGetValue(sessionId, out var player) ? player : null;
+ }
}
\ No newline at end of file
diff --git a/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginContext.cs b/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginContext.cs
index 9108fb861..4468e4ee0 100644
--- a/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginContext.cs
+++ b/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginContext.cs
@@ -16,8 +16,6 @@ internal class PluginContext : IDisposable
public PluginStatus? Status { get; set; }
public BasePlugin? Plugin { get; set; }
- public bool NeedReloadOnMapStart { get; set; } = false;
-
public PluginLoader? Loader { get; set; }
public void Dispose()
diff --git a/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginManager.cs b/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginManager.cs
index 4253be0a2..a887113a5 100644
--- a/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginManager.cs
+++ b/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginManager.cs
@@ -10,387 +10,385 @@
using SwiftlyS2.Shared.Plugins;
using SwiftlyS2.Core.Modules.Plugins;
using System.Runtime.InteropServices;
-using SwiftlyS2.Core.Events;
+using Semver;
namespace SwiftlyS2.Core.Plugins;
internal class PluginManager : IPluginManager
{
- private readonly IServiceProvider rootProvider;
- private readonly RootDirService rootDirService;
- private readonly DataDirectoryService dataDirectoryService;
- private readonly ILogger logger;
-
- private readonly InterfaceManager interfaceManager;
- private readonly List sharedTypes;
- private readonly List plugins;
- private readonly ConcurrentDictionary fileLastChange;
- private readonly ConcurrentDictionary fileReloadTokens;
-
- private readonly FileSystemWatcher? fileWatcher;
+ private const int DefaultReloadDebounceSeconds = 2;
+ private const int WindowsDelayMs = 300;
+ private const int LinuxDelayMs = 5000;
+ private const int MaxFileAccessRetries = 10;
+ private const int InitialFileAccessDelayMs = 50;
+
+ private readonly IServiceProvider _rootProvider;
+ private readonly RootDirService _rootDirService;
+ private readonly DataDirectoryService _dataDirectoryService;
+ private readonly ILogger _logger;
+ private readonly InterfaceManager _interfaceManager;
+ private readonly List _sharedTypes;
+ private readonly List _plugins;
+ private readonly ConcurrentDictionary _fileLastChange;
+ private readonly ConcurrentDictionary _fileReloadTokens;
+ private readonly ConcurrentDictionary _pluginLoadErrors;
+ private readonly FileSystemWatcher? _fileWatcher;
public PluginManager(
IServiceProvider provider,
ILogger logger,
RootDirService rootDirService,
- DataDirectoryService dataDirectoryService
- )
- {
- this.rootProvider = provider;
- this.rootDirService = rootDirService;
- this.dataDirectoryService = dataDirectoryService;
- this.logger = logger;
-
- this.interfaceManager = new();
- this.sharedTypes = [];
- this.plugins = [];
- this.fileLastChange = new ConcurrentDictionary();
- this.fileReloadTokens = new ConcurrentDictionary();
-
- this.fileWatcher = new FileSystemWatcher {
- Path = rootDirService.GetPluginsRoot(),
+ DataDirectoryService dataDirectoryService )
+ {
+ _rootProvider = provider;
+ _rootDirService = rootDirService;
+ _dataDirectoryService = dataDirectoryService;
+ _logger = logger;
+ _interfaceManager = new InterfaceManager();
+ _sharedTypes = [];
+ _plugins = [];
+ _fileLastChange = new ConcurrentDictionary();
+ _fileReloadTokens = new ConcurrentDictionary();
+ _pluginLoadErrors = new ConcurrentDictionary();
+
+ if (NativeServerHelpers.UseAutoHotReload())
+ {
+ _fileWatcher = InitializeFileWatcher();
+ }
+
+ ConfigureAssemblyResolver();
+ }
+
+ internal void Initialize()
+ {
+ LoadExports();
+
+ if (!NativeCore.PluginManualLoadState())
+ {
+ LoadPlugins();
+ }
+ else
+ {
+ LoadPluginsInOrder(NativeCore.PluginLoadOrder());
+ }
+ }
+
+ private FileSystemWatcher InitializeFileWatcher()
+ {
+ var watcher = new FileSystemWatcher {
+ Path = _rootDirService.GetPluginsRoot(),
Filter = "*.dll",
IncludeSubdirectories = true,
NotifyFilter = NotifyFilters.LastWrite,
EnableRaisingEvents = true
};
- this.fileWatcher.Changed += ( sender, e ) =>
- {
- static async Task WaitForFileAccess( CancellationToken token, string filePath, int maxRetries = 10, int initialDelayMs = 50, ILogger? logger = null )
- {
- for (var i = 1; i <= maxRetries && !token.IsCancellationRequested; i++)
- {
- try
- {
- using var stream = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
- break;
- }
- catch (IOException)
- {
- if (i < maxRetries)
- {
- // 50ms, 100ms, 200ms, 400ms, 800ms...
- await Task.Delay(initialDelayMs * (1 << (i - 1)), token);
- }
- continue;
- }
- catch (Exception)
- {
- break;
- }
- }
- }
- try
- {
- if (!NativeServerHelpers.UseAutoHotReload() || e.ChangeType != WatcherChangeTypes.Changed)
- {
- return;
- }
+ watcher.Changed += OnPluginFileChanged;
+ watcher.Created += OnPluginFileCreated;
- var pluginDirectory = Path.GetDirectoryName(e.FullPath) ?? string.Empty;
- var directoryName = Path.GetFileName(pluginDirectory) ?? string.Empty;
- var fileName = Path.GetFileNameWithoutExtension(e.FullPath);
- if (string.IsNullOrWhiteSpace(directoryName) || !fileName.Equals(directoryName))
- {
- return;
- }
+ return watcher;
+ }
- if ((DateTime.UtcNow - fileLastChange.GetValueOrDefault(directoryName, DateTime.MinValue)).TotalSeconds > 2)
- {
- _ = fileLastChange.AddOrUpdate(directoryName, DateTime.UtcNow, ( _, _ ) => DateTime.UtcNow);
-
- var context = plugins.Find(x => pluginDirectory.Equals(x.PluginDirectory, StringComparison.CurrentCultureIgnoreCase));
- if (context != null)
- {
- var plugin = context.Plugin;
- if (plugin != null)
- {
- var method = plugin.ReloadMethod;
- if (method == PluginReloadMethod.OnMapChange)
- {
- context.NeedReloadOnMapStart = true;
- logger.LogInformation("Found plugin file update, it will be reloaded on next map start: {name}", directoryName);
- return;
- }
- if (method == PluginReloadMethod.OnlyByCommand)
- {
- logger.LogInformation("Found plugin file update, but its auto hot-reload is disabled: {name}", directoryName);
- return;
- }
- }
- }
- if (fileReloadTokens.TryRemove(directoryName, out var oldCts))
- {
- oldCts.Cancel();
- oldCts.Dispose();
- }
-
- var cts = new CancellationTokenSource();
- _ = fileReloadTokens.AddOrUpdate(directoryName, cts, ( _, _ ) => cts);
-
- // Wait for file to be accessible, then reload
- _ = Task.Run(async () =>
- {
- try
- {
- var wait = 300;
- if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
- {
- wait = 5000;
- logger.LogWarning("Detected Linux OS, a 5 second delay for reload.");
- }
- await Task.Delay(wait, cts.Token);
-
- await WaitForFileAccess(cts.Token, e.FullPath, logger: logger);
- var pdbFile = Path.ChangeExtension(e.FullPath, ".pdb");
- if (File.Exists(pdbFile))
- {
- await WaitForFileAccess(cts.Token, pdbFile, logger: logger);
- }
-
- if (ReloadPluginByDllName(directoryName, true))
- {
- logger.LogInformation("Reloaded plugin: {Format}", directoryName);
- }
- else
- {
- logger.LogWarning("Failed to reload plugin: {Format}", directoryName);
- }
- }
- catch (Exception ex)
- {
- if (GlobalExceptionHandler.Handle(ex))
- {
- AnsiConsole.WriteException(ex);
- }
- }
- }, cts.Token);
- }
- }
- catch (Exception ex)
- {
- if (!GlobalExceptionHandler.Handle(ex))
- {
- return;
- }
- logger.LogError(ex, "Failed to handle plugin change");
- }
+ private void ConfigureAssemblyResolver()
+ {
+ AppDomain.CurrentDomain.AssemblyResolve += ( sender, e ) =>
+ {
+ var assemblyName = new AssemblyName(e.Name).Name ?? string.Empty;
+
+ return assemblyName.Equals("SwiftlyS2.CS2", StringComparison.OrdinalIgnoreCase)
+ ? Assembly.GetExecutingAssembly()
+ : AppDomain.CurrentDomain.GetAssemblies()
+ .FirstOrDefault(a => assemblyName.Equals(a.GetName().Name, StringComparison.OrdinalIgnoreCase));
};
+ }
- this.fileWatcher.Created += ( sender, e ) =>
- {
- static async Task WaitForFileAccess( CancellationToken token, string filePath, int maxRetries = 10, int initialDelayMs = 50 )
- {
- for (var i = 1; i <= maxRetries && !token.IsCancellationRequested; i++)
- {
- try
- {
- using var stream = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
- break;
- }
- catch (IOException)
- {
- if (i < maxRetries)
- {
- await Task.Delay(initialDelayMs * (1 << (i - 1)), token);
- }
- continue;
- }
- catch (Exception)
- {
- break;
- }
- }
- }
+ public IReadOnlyList GetPlugins() => _plugins.AsReadOnly();
- try
- {
- if (!NativeServerHelpers.UseAutoHotReload() || e.ChangeType != WatcherChangeTypes.Created)
- {
- return;
- }
+ public bool LoadPlugin( string pluginId, bool silent = false )
+ => LoadPluginById(pluginId, silent);
- var pluginDirectory = Path.GetDirectoryName(e.FullPath) ?? string.Empty;
- var directoryName = Path.GetFileName(pluginDirectory) ?? string.Empty;
- var fileName = Path.GetFileNameWithoutExtension(e.FullPath);
+ public bool UnloadPlugin( string pluginId, bool silent = false )
+ => UnloadPluginById(pluginId, silent);
- if (string.IsNullOrWhiteSpace(directoryName) || !fileName.Equals(directoryName))
- {
- return;
- }
+ public bool ReloadPlugin( string pluginId, bool silent = false )
+ => ReloadPluginById(pluginId, silent);
- // Check if this plugin already exists
- var existingContext = plugins.Find(x => pluginDirectory.Equals(x.PluginDirectory, StringComparison.CurrentCultureIgnoreCase));
- if (existingContext != null)
- {
- logger.LogInformation("Plugin already loaded, skipping: {name}", directoryName);
- return;
- }
+ public PluginStatus? GetPluginStatus( string pluginId )
+ => FindPluginById(pluginId)?.Status;
+
+ public PluginStatus? GetPluginStatusByDllName( string dllName )
+ {
+ var pluginDir = FindPluginDirectoryByDllName(dllName);
+ return string.IsNullOrWhiteSpace(pluginDir)
+ ? null
+ : FindPluginByDirectory(pluginDir)?.Status;
+ }
- var cts = new CancellationTokenSource();
+ public PluginMetadata? GetPluginMetadata( string pluginId )
+ => FindPluginById(pluginId)?.Metadata;
- // Wait for file to be accessible, then load the new plugin
- _ = Task.Run(async () =>
- {
- try
- {
- var wait = 300;
- if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
- {
- wait = 5000;
- logger.LogWarning("Detected Linux OS, a 5 second delay for loading new plugin.");
- }
- await Task.Delay(wait, cts.Token);
-
- await WaitForFileAccess(cts.Token, e.FullPath);
- var pdbFile = Path.ChangeExtension(e.FullPath, ".pdb");
- if (File.Exists(pdbFile))
- {
- await WaitForFileAccess(cts.Token, pdbFile);
- }
-
- // Load the new plugin
- var context = LoadPlugin(pluginDirectory, false, silent: false);
- if (context?.Status == PluginStatus.Loaded)
- {
- var relativePath = Path.GetRelativePath(rootDirService.GetRoot(), pluginDirectory);
- var displayPath = Path.Join("(swRoot)", relativePath);
-
- logger.LogInformation(
- string.Join("\n", [
- "Hot-loaded New Plugin",
- "├─ {Id} {Version}",
- "├─ Author: {Author}",
- "└─ Path: {RelativePath}"
- ]),
- context.Metadata!.Id,
- context.Metadata!.Version,
- context.Metadata!.Author,
- displayPath
- );
- }
- else
- {
- logger.LogWarning("Failed to hot-load new plugin: {Format}", directoryName);
- }
- }
- catch (Exception ex)
- {
- if (GlobalExceptionHandler.Handle(ex))
- {
- AnsiConsole.WriteException(ex);
- }
- logger.LogError(ex, "Failed to hot-load new plugin: {Format}", directoryName);
- }
- finally
- {
- cts.Dispose();
- }
- }, cts.Token);
+ public string? GetPluginPath( string pluginId )
+ => FindPluginById(pluginId)?.PluginDirectory;
+
+ public Dictionary GetPluginPaths()
+ => _plugins
+ .Where(p => p.Metadata != null && p.PluginDirectory != null)
+ .ToDictionary(p => p.Metadata!.Id, p => p.PluginDirectory!);
+
+ public Dictionary GetAllPluginStatuses()
+ => _plugins
+ .Where(p => p.Metadata != null)
+ .ToDictionary(p => p.Metadata!.Id, p => p.Status ?? PluginStatus.Indeterminate);
+
+ public Dictionary GetAllPluginMetadata()
+ => _plugins
+ .Where(p => p.Metadata != null)
+ .ToDictionary(p => p.Metadata!.Id, p => p.Metadata!);
+
+ public IEnumerable GetAllPlugins()
+ => _plugins
+ .Where(p => p.Metadata != null)
+ .Select(p => p.Metadata!.Id);
+
+ public Dictionary GetPluginLoadErrors()
+ => new Dictionary(_pluginLoadErrors);
+
+ public void RegenerateTranslations()
+ {
+ foreach (var plugin in _plugins.Where(p => p.Core != null))
+ {
+ plugin.Core!.TranslationService.CreateTranslationResource();
+ }
+ }
+
+ public bool LoadPluginById( string id, bool silent = false )
+ {
+ var context = FindPluginById(id, excludeStatuses: [PluginStatus.Loading, PluginStatus.Loaded]);
+ if (context == null || string.IsNullOrWhiteSpace(context.PluginDirectory))
+ {
+ if (!silent) _logger.LogWarning("Failed to load plugin by id: {Id}", id);
+ return false;
+ }
+
+ return LoadPluginByDllName(Path.GetFileName(context.PluginDirectory), hotReload: false, silent);
+ }
+
+ public bool LoadPluginByDllName( string dllName, bool hotReload, bool silent = false )
+ {
+ var pluginDir = FindPluginDirectoryByDllName(dllName);
+ if (string.IsNullOrWhiteSpace(pluginDir))
+ {
+ if (!silent) _logger.LogWarning("Failed to load plugin by name: {DllName}", dllName);
+ return false;
+ }
+
+ var oldContext = FindPluginByDirectory(pluginDir, excludeStatuses: [PluginStatus.Loading, PluginStatus.Loaded]);
+ PluginContext? newContext = null;
+
+ try
+ {
+ if (oldContext != null && _plugins.Remove(oldContext))
+ {
+ newContext = LoadPluginInternal(pluginDir, hotReload, silent);
}
- catch (Exception ex)
+ else if (oldContext == null)
{
- if (!GlobalExceptionHandler.Handle(ex))
+ newContext = LoadPluginInternal(pluginDir, hotReload, silent);
+ }
+
+ if (newContext?.Status == PluginStatus.Loaded)
+ {
+ if (!silent)
{
- return;
+ _logger.LogInformation("Loaded plugin: {Id}", newContext.Metadata!.Id);
}
- logger.LogError(ex, "Failed to handle new plugin creation");
+ return true;
}
- };
- AppDomain.CurrentDomain.AssemblyResolve += ( sender, e ) =>
+ throw new InvalidOperationException($"Failed to load plugin from: {pluginDir}");
+ }
+ catch (Exception e)
{
- var loadingAssemblyName = new AssemblyName(e.Name).Name ?? string.Empty;
- return loadingAssemblyName.Equals("SwiftlyS2.CS2", StringComparison.OrdinalIgnoreCase)
- ? Assembly.GetExecutingAssembly()
- : AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => loadingAssemblyName == a.GetName().Name);
- };
- EventPublisher.InternalOnMapLoad += () =>
+ if (GlobalExceptionHandler.Handle(e))
+ {
+ _logger.LogWarning(e, "Failed to load plugin by name: {Path}", pluginDir);
+ var pluginName = Path.GetFileName(pluginDir);
+ _pluginLoadErrors[pluginName] = e.ToString();
+ }
+ return false;
+ }
+ finally
{
- var array = plugins.Where(x => x.NeedReloadOnMapStart).ToArray();
- foreach (var p in array)
+ RebuildSharedServices();
+ }
+ }
+
+ private void LoadPluginsInOrder( string loadOrder )
+ {
+ var plugins = loadOrder.Split('\x01');
+ foreach (var plugin in plugins)
+ {
+ _ = LoadPluginById(plugin, silent: false);
+ }
+ }
+
+ private void LoadPlugins()
+ {
+ EnumeratePluginDirectories(_rootDirService.GetPluginsRoot(), pluginDir =>
+ {
+ var displayPath = GetDisplayPath(pluginDir);
+ var dllName = Path.GetFileName(pluginDir);
+ var fullDisplayPath = Path.Join(displayPath, $"{dllName}.dll");
+
+ _logger.LogInformation("Loading plugin: {Path}", fullDisplayPath);
+
+ try
{
- p.NeedReloadOnMapStart = false;
- var directoryName = Path.GetFileName(p.PluginDirectory) ?? string.Empty;
- if (ReloadPluginByDllName(directoryName, true))
+ var context = LoadPluginInternal(pluginDir, hotReload: false, silent: false);
+ if (context?.Status == PluginStatus.Loaded)
{
- logger.LogInformation("Reloaded plugin on map start: {Format}", directoryName);
+ LogPluginLoadSuccess(context, displayPath);
}
else
{
- logger.LogWarning("Failed to reload plugin: {Format}", directoryName);
+ _logger.LogWarning("Failed to load plugin: {Path}", fullDisplayPath);
+ _pluginLoadErrors[dllName] = "Plugin failed to load (status not loaded)";
+ context?.Status = PluginStatus.Error;
}
}
- };
+ catch (Exception e)
+ {
+ if (GlobalExceptionHandler.Handle(e))
+ {
+ _logger.LogWarning(e, "Failed to load plugin: {Path}", fullDisplayPath);
+ _pluginLoadErrors[dllName] = e.ToString();
+ }
+ }
+ });
+
+ RebuildSharedServices();
+ NotifyAllPluginsLoaded();
}
- ///
- /// Must be called after DI container is fully built to avoid circular dependency.
- ///
- internal void Initialize()
+ private PluginContext? LoadPluginInternal( string directory, bool hotReload, bool silent = false )
{
- LoadExports();
- if (!NativeCore.PluginManualLoadState()) LoadPlugins();
- else
+ var existingContext = _plugins.FirstOrDefault(p =>
+ p.PluginDirectory?.Trim().Equals(directory.Trim(), StringComparison.OrdinalIgnoreCase) ?? false);
+
+ if (existingContext != null)
{
- var plugins = NativeCore.PluginLoadOrder().Split('\x01');
- foreach (var plugin in plugins)
- {
- _ = LoadPluginById(plugin, silent: false);
- }
+ _ = _plugins.Remove(existingContext);
}
- }
- public IReadOnlyList GetPlugins() => plugins.AsReadOnly();
+ var context = new PluginContext {
+ PluginDirectory = directory,
+ Status = PluginStatus.Loading
+ };
+ _plugins.Add(context);
- public string? FindPluginDirectoryByDllName( string dllName )
- {
- dllName = dllName.Trim();
- if (dllName.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
+ var entrypointDll = GetPluginEntrypointPath(directory);
+ if (!File.Exists(entrypointDll))
{
- dllName = dllName[..^4];
+ return FailWithError(context, silent, $"Plugin entrypoint DLL not found: {entrypointDll}");
}
- var pluginDir = plugins
- .FirstOrDefault(p => Path.GetFileName(p.PluginDirectory)?.Trim().Equals(dllName.Trim()) ?? false)
- ?.PluginDirectory;
+ var loader = CreatePluginLoader(entrypointDll);
+ var pluginType = FindPluginType(loader);
+ if (pluginType == null)
+ {
+ return FailWithError(context, silent, $"No plugin type found in: {entrypointDll}");
+ }
- if (!string.IsNullOrWhiteSpace(pluginDir))
+ var metadata = pluginType.GetCustomAttribute();
+ if (metadata == null)
{
- return pluginDir;
+ return FailWithError(context, silent, $"Plugin metadata not found in: {entrypointDll}");
}
- string? foundDir = null;
- EnumeratePluginDirectories(rootDirService.GetPluginsRoot(), dir =>
+ context.Metadata = metadata;
+
+ var coreVersion = NativeEngineHelpers.GetNativeVersion();
+ var minimumApiVersion = context.Metadata.MinimumAPIVersion ?? "0.0.0";
+
+ SemVersion? coreSemver = null;
+ try
{
- if (Path.GetFileName(dir).Equals(dllName))
+ coreSemver = SemVersion.Parse(coreVersion, SemVersionStyles.AllowV);
+ }
+ catch (Exception) { }
+
+ if (coreSemver != null)
+ {
+ SemVersion? minApiSemver = null;
+ try
{
- foundDir = dir;
+ minApiSemver = SemVersion.Parse(minimumApiVersion, SemVersionStyles.AllowV);
+ }
+ catch (Exception e)
+ {
+ if (GlobalExceptionHandler.Handle(e))
+ {
+ _logger.LogWarning(e, "Failed to parse minimum API version for plugin {Id}: '{Version}'. Falling back to '0.0.0'.", context.Metadata.Id, minimumApiVersion);
+ }
+ minApiSemver = new SemVersion(0, 0, 0);
}
- });
- return foundDir;
+ var r = SemVersion.CompareSortOrder(coreSemver, minApiSemver);
+
+ if (r < 0)
+ {
+ return FailWithError(context, silent, $"Plugin API version '{minimumApiVersion}' is newer than the core version '{coreVersion}'.");
+ }
+ }
+
+ _dataDirectoryService.EnsurePluginDataDirectory(metadata.Id);
+
+ var core = CreateSwiftlyCore(metadata, pluginType, directory);
+ var plugin = InstantiatePlugin(pluginType, core);
+
+ try
+ {
+ plugin.Load(hotReload);
+ context.Status = PluginStatus.Loaded;
+ context.Core = core;
+ context.Plugin = plugin;
+ context.Loader = loader;
+
+ var pluginName = Path.GetFileName(directory);
+ _ = _pluginLoadErrors.TryRemove(pluginName, out _);
+
+ return context;
+ }
+ catch (Exception e)
+ {
+ _ = GlobalExceptionHandler.Handle(e);
+ CleanupFailedPlugin(plugin, loader, core);
+ _logger.LogError(e, "Exception occurred while loading plugin: {PluginPath}", entrypointDll);
+ var pluginName = Path.GetFileName(directory);
+ _pluginLoadErrors[pluginName] = e.ToString();
+ return FailWithError(context, silent, $"Failed to load plugin: {entrypointDll}");
+ }
}
public bool UnloadPluginById( string id, bool silent = false, bool rebuild = true )
{
- var context = plugins
- .Where(p => p.Status != PluginStatus.Unloaded)
- .FirstOrDefault(p => p.Metadata?.Id.Trim().Equals(id.Trim(), StringComparison.OrdinalIgnoreCase) ?? false);
+ var context = FindPluginById(id, excludeStatuses: [PluginStatus.Unloaded]);
+ if (context == null)
+ {
+ if (!silent) _logger.LogWarning("Plugin not found: {Id}", id);
+ return false;
+ }
try
{
- context?.Dispose();
- context?.Loader?.Dispose();
- context?.Core?.Dispose();
- context!.Status = PluginStatus.Unloaded;
+ context.Dispose();
+ _ = _plugins.Remove(context);
return true;
}
catch
{
- logger.LogWarning("Failed to unload plugin by id: {Id}", id);
- context!.Status = PluginStatus.Indeterminate;
+ if (!silent) _logger.LogWarning("Failed to unload plugin: {Id}", id);
+ context.Status = PluginStatus.Indeterminate;
return false;
}
finally
@@ -407,204 +405,245 @@ public bool UnloadPluginByDllName( string dllName, bool silent = false, bool reb
var pluginDir = FindPluginDirectoryByDllName(dllName);
if (string.IsNullOrWhiteSpace(pluginDir))
{
- logger.LogWarning("Failed to find plugin by name: {DllName}", dllName);
+ if (!silent) _logger.LogWarning("Failed to find plugin by name: {DllName}", dllName);
return false;
}
- var context = plugins
- .Where(p => p.Status != PluginStatus.Unloaded)
- .FirstOrDefault(p => p.PluginDirectory?.Trim().Equals(pluginDir.Trim()) ?? false);
-
- if (string.IsNullOrWhiteSpace(context?.Metadata?.Id))
+ var context = FindPluginByDirectory(pluginDir, excludeStatuses: [PluginStatus.Unloaded]);
+ if (context?.Metadata == null)
{
- logger.LogWarning("Failed to find plugin by name: {DllName}", dllName);
+ if (!silent) _logger.LogWarning("Failed to find plugin by name: {DllName}", dllName);
return false;
}
return UnloadPluginById(context.Metadata.Id, silent, rebuild);
}
- public bool LoadPluginById( string id, bool silent = false )
+ public bool ReloadPluginById( string id, bool silent = false )
{
- var context = plugins
- .Where(p => p.Status != PluginStatus.Loading && p.Status != PluginStatus.Loaded)
- .FirstOrDefault(p => p.Metadata?.Id.Trim().Equals(id.Trim(), StringComparison.OrdinalIgnoreCase) ?? false);
+ _ = UnloadPluginById(id, silent, rebuild: false);
+ return LoadPluginById(id, silent);
+ }
- if (string.IsNullOrWhiteSpace(context?.PluginDirectory))
+ public bool ReloadPluginByDllName( string dllName, bool silent = false )
+ {
+ _ = UnloadPluginByDllName(dllName, silent, rebuild: false);
+ return LoadPluginByDllName(dllName, hotReload: true, silent);
+ }
+
+ private void LoadExports()
+ {
+ try
{
- logger.LogWarning("Failed to load plugin by id: {Id}", id);
- return false;
+ var resolver = new DependencyResolver(_logger);
+ resolver.AnalyzeDependencies(_rootDirService.GetPluginsRoot());
+
+ _logger.LogInformation("{Graph}", resolver.GetDependencyGraphVisualization());
+
+ var loadOrder = resolver.GetLoadOrder();
+ _logger.LogInformation("Loading {Count} export assemblies in dependency order", loadOrder.Count);
+
+ foreach (var exportFile in loadOrder)
+ {
+ LoadExportAssembly(exportFile);
+ }
+
+ _logger.LogInformation("Loaded {Count} shared types", _sharedTypes.Count);
+ }
+ catch (InvalidOperationException ex) when (ex.Message.Contains("circular dependency", StringComparison.OrdinalIgnoreCase))
+ {
+ _logger.LogError(ex, "Circular dependency detected in plugin exports, loading manually");
+ LoadExportsManually(_rootDirService.GetPluginsRoot());
+ }
+ catch (Exception ex)
+ {
+ if (GlobalExceptionHandler.Handle(ex))
+ {
+ _logger.LogError(ex, "Failed to load exports");
+ }
+ }
+ }
+
+ private void LoadExportAssembly( string exportFile )
+ {
+ try
+ {
+ var assembly = Assembly.LoadFrom(exportFile);
+ var exports = assembly.GetTypes();
+ _logger.LogDebug("Loaded {Count} types from {Path}", exports.Length, Path.GetFileName(exportFile));
+ _sharedTypes.AddRange(exports);
+ }
+ catch (Exception ex)
+ {
+ if (GlobalExceptionHandler.Handle(ex))
+ {
+ _logger.LogWarning(ex, "Failed to load export assembly: {Path}", exportFile);
+ }
+ }
+ }
+
+ private void LoadExportsManually( string startDirectory )
+ {
+ EnumeratePluginDirectories(startDirectory, pluginDir =>
+ {
+ var exportsPath = Path.Combine(pluginDir, "resources", "exports");
+ if (!Directory.Exists(exportsPath))
+ {
+ return;
+ }
+
+ foreach (var exportFile in Directory.GetFiles(exportsPath, "*.dll"))
+ {
+ LoadExportAssembly(exportFile);
+ }
+ });
+ }
+
+ private void OnPluginFileChanged( object sender, FileSystemEventArgs e )
+ {
+ if (!ShouldProcessFileChange(e, out var directoryName))
+ {
+ return;
+ }
+
+ try
+ {
+ if (!ShouldDebounceReload(directoryName))
+ {
+ return;
+ }
+
+ CancelPreviousReload(directoryName);
+ var cts = RegisterNewReload(directoryName);
+ SchedulePluginReload(e.FullPath, directoryName, cts);
+ }
+ catch (Exception ex)
+ {
+ if (GlobalExceptionHandler.Handle(ex))
+ {
+ _logger.LogError(ex, "Failed to handle plugin change");
+ }
}
-
- return LoadPluginByDllName(Path.GetFileName(context.PluginDirectory), silent);
}
- public bool LoadPluginByDllName( string dllName, bool silent = false )
+ private void OnPluginFileCreated( object sender, FileSystemEventArgs e )
{
- var pluginDir = FindPluginDirectoryByDllName(dllName);
- if (string.IsNullOrWhiteSpace(pluginDir))
+ if (!ShouldProcessFileChange(e, out var directoryName))
{
- logger.LogWarning("Failed to load plugin by name: {DllName}", dllName);
- return false;
+ return;
}
- var oldContext = plugins
- .Where(p => p.Status != PluginStatus.Loading && p.Status != PluginStatus.Loaded)
- .FirstOrDefault(p => p.PluginDirectory?.Trim().Equals(pluginDir.Trim()) ?? false);
-
- PluginContext? newContext = null;
try
{
- // If plugin exists and is not loaded, reload it
- if (oldContext != null && plugins.Remove(oldContext))
+ var pluginDirectory = Path.GetDirectoryName(e.FullPath) ?? string.Empty;
+ if (FindPluginByDirectory(pluginDirectory) != null)
{
- newContext = LoadPlugin(pluginDir, true, silent);
- if (newContext?.Status == PluginStatus.Loaded)
- {
- if (!silent)
- {
- logger.LogInformation("Loaded plugin: {Id}", newContext.Metadata!.Id);
- }
- return true;
- }
- }
- // If plugin doesn't exist in the list, it's a new plugin - load it
- else if (oldContext == null)
- {
- newContext = LoadPlugin(pluginDir, false, silent);
- if (newContext?.Status == PluginStatus.Loaded)
- {
- if (!silent)
- {
- logger.LogInformation("Loaded new plugin: {Id}", newContext.Metadata!.Id);
- }
- return true;
- }
+ _logger.LogInformation("Plugin already loaded, skipping: {Name}", directoryName);
+ return;
}
- throw new ArgumentException(string.Empty, string.Empty);
+
+ var cts = new CancellationTokenSource();
+ SchedulePluginLoad(e.FullPath, pluginDirectory, directoryName, cts);
}
- catch (Exception e)
+ catch (Exception ex)
{
- if (!GlobalExceptionHandler.Handle(e))
+ if (GlobalExceptionHandler.Handle(ex))
{
- return false;
+ _logger.LogError(ex, "Failed to handle new plugin creation");
}
- logger.LogWarning(e, "Failed to load plugin by name: {Path}", pluginDir);
+ }
+ }
+
+ private bool ShouldProcessFileChange( FileSystemEventArgs e, out string directoryName )
+ {
+ directoryName = string.Empty;
+
+ if (!NativeServerHelpers.UseAutoHotReload())
+ {
return false;
}
- finally
+
+ var pluginDirectory = Path.GetDirectoryName(e.FullPath) ?? string.Empty;
+ directoryName = Path.GetFileName(pluginDirectory) ?? string.Empty;
+ var fileName = Path.GetFileNameWithoutExtension(e.FullPath);
+
+ return !string.IsNullOrWhiteSpace(directoryName) && fileName.Equals(directoryName);
+ }
+
+ private bool ShouldDebounceReload( string directoryName )
+ {
+ var lastChange = _fileLastChange.GetValueOrDefault(directoryName, DateTime.MinValue);
+ var timeSinceLastChange = (DateTime.UtcNow - lastChange).TotalSeconds;
+
+ if (timeSinceLastChange > DefaultReloadDebounceSeconds)
{
- RebuildSharedServices();
+ _ = _fileLastChange.AddOrUpdate(directoryName, DateTime.UtcNow, ( _, _ ) => DateTime.UtcNow);
+ return true;
}
+
+ return false;
}
- public bool ReloadPluginById( string id, bool silent = false )
+ private void CancelPreviousReload( string directoryName )
{
- _ = UnloadPluginById(id, silent, false);
- return LoadPluginById(id, silent);
+ if (_fileReloadTokens.TryRemove(directoryName, out var oldCts))
+ {
+ oldCts.Cancel();
+ oldCts.Dispose();
+ }
}
- public bool ReloadPluginByDllName( string dllName, bool silent = false )
+ private CancellationTokenSource RegisterNewReload( string directoryName )
{
- _ = UnloadPluginByDllName(dllName, silent: true, rebuild: false);
- return LoadPluginByDllName(dllName, silent);
+ var cts = new CancellationTokenSource();
+ _ = _fileReloadTokens.AddOrUpdate(directoryName, cts, ( _, _ ) => cts);
+ return cts;
}
- private void LoadExports()
+ private void SchedulePluginReload( string filePath, string directoryName, CancellationTokenSource cts )
{
- void PopulateSharedManually( string startDirectory )
+ _ = Task.Run(async () =>
{
- EnumeratePluginDirectories(startDirectory, pluginDir =>
+ try
{
- var exportsPath = Path.Combine(pluginDir, "resources", "exports");
- if (!Directory.Exists(exportsPath))
- {
- return;
- }
-
- Directory.GetFiles(exportsPath, "*.dll")
- .ToList()
- .ForEach(exportFile =>
- {
- try
- {
- var assembly = Assembly.LoadFrom(exportFile);
- assembly.GetTypes().ToList().ForEach(sharedTypes.Add);
- }
- catch (Exception innerEx)
- {
- if (!GlobalExceptionHandler.Handle(innerEx))
- {
- return;
- }
- logger.LogWarning(innerEx, "Failed to load export assembly: {Path}", exportFile);
- }
- });
- });
- }
-
- try
- {
- var resolver = new DependencyResolver(logger);
- resolver.AnalyzeDependencies(rootDirService.GetPluginsRoot());
- logger.LogInformation("{Graph}", resolver.GetDependencyGraphVisualization());
- var loadOrder = resolver.GetLoadOrder();
- logger.LogInformation("Loading {Count} export assemblies in dependency order", loadOrder.Count);
+ await WaitForPlatformDelay(cts.Token);
+ await WaitForFileAccessibility(filePath, cts.Token);
- loadOrder.ForEach(exportFile =>
- {
- try
+ if (ReloadPluginByDllName(directoryName, silent: true))
{
- var assembly = Assembly.LoadFrom(exportFile);
- var exports = assembly.GetTypes();
- logger.LogDebug("Loaded {Count} types from {Path}", exports.Length, Path.GetFileName(exportFile));
- exports.ToList().ForEach(sharedTypes.Add);
+ _logger.LogInformation("Reloaded plugin: {Name}", directoryName);
}
- catch (Exception ex)
+ else
{
- if (!GlobalExceptionHandler.Handle(ex))
- {
- return;
- }
- logger.LogWarning(ex, "Failed to load export assembly: {Path}", exportFile);
+ _logger.LogWarning("Failed to reload plugin: {Name}", directoryName);
}
- });
-
- logger.LogInformation("Loaded {Count} shared types", sharedTypes.Count);
- }
- catch (InvalidOperationException ex) when (ex.Message.Contains("circular dependency", StringComparison.OrdinalIgnoreCase))
- {
- logger.LogError(ex, "Circular dependency detected in plugin exports, loading manually");
- PopulateSharedManually(rootDirService.GetPluginsRoot());
- }
- catch (Exception ex)
- {
- if (!GlobalExceptionHandler.Handle(ex))
+ }
+ catch (Exception ex)
{
- return;
+ if (GlobalExceptionHandler.Handle(ex))
+ {
+ AnsiConsole.WriteException(ex);
+ }
}
- logger.LogError(ex, "Failed to load exports");
- }
+ }, cts.Token);
}
- private void LoadPlugins()
+
+ private void SchedulePluginLoad( string filePath, string pluginDirectory, string directoryName, CancellationTokenSource cts )
{
- EnumeratePluginDirectories(rootDirService.GetPluginsRoot(), pluginDir =>
+ _ = Task.Run(async () =>
{
- var relativePath = Path.GetRelativePath(rootDirService.GetRoot(), pluginDir);
- var displayPath = Path.Join("(swRoot)", relativePath);
- var dllName = Path.GetFileName(pluginDir);
- var fullDisplayPath = string.IsNullOrWhiteSpace(displayPath) ? string.Empty : $"{Path.Join(displayPath, dllName)}.dll";
-
- logger.LogInformation("Loading plugin: {Path}", fullDisplayPath);
-
try
{
- var context = LoadPlugin(pluginDir, false, silent: false);
+ await WaitForPlatformDelay(cts.Token);
+ await WaitForFileAccessibility(filePath, cts.Token);
+
+ var context = LoadPluginInternal(pluginDirectory, hotReload: true, silent: false);
if (context?.Status == PluginStatus.Loaded)
{
- logger.LogInformation(
+ var displayPath = GetDisplayPath(pluginDirectory);
+ _logger.LogInformation(
string.Join("\n", [
- "Loaded Plugin",
+ "Hot-loaded New Plugin",
"├─ {Id} {Version}",
"├─ Author: {Author}",
"└─ Path: {RelativePath}"
@@ -617,257 +656,293 @@ private void LoadPlugins()
}
else
{
- logger.LogWarning("Failed to load plugin: {Path}", fullDisplayPath);
+ _logger.LogWarning("Failed to hot-load new plugin: {Name}", directoryName);
}
}
- catch (Exception e)
+ catch (Exception ex)
{
- if (!GlobalExceptionHandler.Handle(e))
+ if (GlobalExceptionHandler.Handle(ex))
{
- return;
+ AnsiConsole.WriteException(ex);
}
- logger.LogWarning(e, "Failed to load plugin: {Path}", fullDisplayPath);
+ _logger.LogError(ex, "Failed to hot-load new plugin: {Name}", directoryName);
}
- });
-
- RebuildSharedServices();
-
- plugins
- .Where(p => p.Status == PluginStatus.Loaded)
- .ToList()
- .ForEach(p => p.Plugin?.OnAllPluginsLoaded());
+ finally
+ {
+ cts.Dispose();
+ }
+ }, cts.Token);
}
- private PluginContext? LoadPlugin( string dir, bool hotReload, bool silent = false )
+ private static async Task WaitForPlatformDelay( CancellationToken token )
{
- PluginContext? FailWithError( PluginContext context, string message )
- {
- logger.LogWarning("{Message}", message);
- context.Status = PluginStatus.Error;
- return null;
- }
+ var delay = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? LinuxDelayMs : WindowsDelayMs;
+ await Task.Delay(delay, token);
+ }
- var context = new PluginContext { PluginDirectory = dir, Status = PluginStatus.Loading };
- plugins.Add(context);
+ private async Task WaitForFileAccessibility( string filePath, CancellationToken token )
+ {
+ await WaitForFileAccess(filePath, token);
- var entrypointDll = Path.Combine(dir, Path.GetFileName(dir) + ".dll");
- if (!File.Exists(entrypointDll))
+ var pdbFile = Path.ChangeExtension(filePath, ".pdb");
+ if (File.Exists(pdbFile))
{
- return FailWithError(context, $"Failed to find plugin entrypoint DLL: {Path.Combine(dir, Path.GetFileName(dir))}.dll");
+ await WaitForFileAccess(pdbFile, token);
}
+ }
- var currentContext = AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly());
- var loader = PluginLoader.CreateFromAssemblyFile(
- entrypointDll,
- [typeof(BasePlugin), .. sharedTypes],
- config =>
+ private static async Task WaitForFileAccess( string filePath, CancellationToken token, int maxRetries = MaxFileAccessRetries, int initialDelayMs = InitialFileAccessDelayMs )
+ {
+ for (var i = 1; i <= maxRetries && !token.IsCancellationRequested; i++)
+ {
+ try
{
- config.IsUnloadable = config.LoadInMemory = true;
- if (currentContext != null)
+ using var stream = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+ return;
+ }
+ catch (IOException)
+ {
+ if (i < maxRetries)
{
- (config.DefaultContext, config.PreferSharedTypes) = (currentContext, true);
+ var delay = initialDelayMs * (1 << (i - 1)); // Exponential backoff
+ await Task.Delay(delay, token);
}
}
- );
-
- var pluginType = loader.LoadDefaultAssembly()
- .GetTypes()
- .FirstOrDefault(t => t.IsSubclassOf(typeof(BasePlugin)));
- if (pluginType == null)
- {
- return FailWithError(context, $"Failed to find plugin type: {Path.Combine(dir, Path.GetFileName(dir))}.dll");
- }
-
- var metadata = pluginType.GetCustomAttribute();
- if (metadata == null)
- {
- return FailWithError(context, $"Failed to find plugin metadata: {Path.Combine(dir, Path.GetFileName(dir))}.dll");
+ catch
+ {
+ return;
+ }
}
+ }
- context.Metadata = metadata;
- dataDirectoryService.EnsurePluginDataDirectory(metadata.Id);
-
- var pluginDir = Path.GetDirectoryName(entrypointDll)!;
- var dataDir = dataDirectoryService.GetPluginDataDirectory(metadata.Id);
- var core = new SwiftlyCore(metadata.Id, pluginDir, metadata, pluginType, rootProvider, dataDir);
+ public string? FindPluginDirectoryByDllName( string dllName )
+ {
+ dllName = dllName.Trim().TrimEnd(".dll", StringComparison.OrdinalIgnoreCase);
- core.InitializeType(pluginType);
- var plugin = (BasePlugin)Activator.CreateInstance(pluginType, [core])!;
- core.InitializeObject(plugin);
+ var pluginDir = _plugins
+ .FirstOrDefault(p => Path.GetFileName(p.PluginDirectory)?.Trim().Equals(dllName, StringComparison.OrdinalIgnoreCase) ?? false)
+ ?.PluginDirectory;
- try
+ if (!string.IsNullOrWhiteSpace(pluginDir))
{
- plugin.Load(hotReload);
- context.Status = PluginStatus.Loaded;
- context.Core = core;
- context.Plugin = plugin;
- context.Loader = loader;
- return context;
+ return pluginDir;
}
- catch (Exception e)
- {
- _ = GlobalExceptionHandler.Handle(e);
- try
- {
- plugin.Unload();
- loader?.Dispose();
- core?.Dispose();
- }
- catch (Exception ex)
+ string? foundDir = null;
+ EnumeratePluginDirectories(_rootDirService.GetPluginsRoot(), dir =>
+ {
+ if (Path.GetFileName(dir).Equals(dllName, StringComparison.OrdinalIgnoreCase))
{
- if (GlobalExceptionHandler.Handle(ex))
- {
- AnsiConsole.WriteException(ex);
- }
+ foundDir = dir;
}
+ });
- logger.LogError(e, "Exception occurred while loading plugin: {PluginPath}", Path.Combine(dir, Path.GetFileName(dir)) + ".dll");
-
- return FailWithError(context, $"Failed to load plugin: {Path.Combine(dir, Path.GetFileName(dir))}.dll");
- }
+ return foundDir;
}
- private void RebuildSharedServices()
+ private PluginContext? FindPluginById( string id, PluginStatus[]? excludeStatuses = null )
{
- interfaceManager.Dispose();
-
- var loadedPlugins = plugins
- .Where(p => p.Status == PluginStatus.Loaded)
- .ToList();
+ var query = _plugins.AsEnumerable();
- loadedPlugins.ForEach(p => p.Plugin?.ConfigureSharedInterface(interfaceManager));
- interfaceManager.Build();
+ if (excludeStatuses != null && excludeStatuses.Length > 0)
+ {
+ query = query.Where(p => !excludeStatuses.Contains(p.Status ?? PluginStatus.Indeterminate));
+ }
- loadedPlugins.ForEach(p => p.Plugin?.UseSharedInterface(interfaceManager));
- loadedPlugins.ForEach(p => p.Plugin?.OnSharedInterfaceInjected(interfaceManager));
+ return query.FirstOrDefault(p =>
+ p.Metadata?.Id.Trim().Equals(id.Trim(), StringComparison.OrdinalIgnoreCase) ?? false);
}
- private static void EnumeratePluginDirectories( string directory, Action action )
+ private PluginContext? FindPluginByDirectory( string directory, PluginStatus[]? excludeStatuses = null )
{
- var pluginDirs = Directory.GetDirectories(directory);
+ var query = _plugins.AsEnumerable();
- foreach (var pluginDir in pluginDirs)
+ if (excludeStatuses != null && excludeStatuses.Length > 0)
{
- var dirName = Path.GetFileName(pluginDir);
- if (dirName.Trim().StartsWith('[') && dirName.EndsWith(']'))
- {
- EnumeratePluginDirectories(pluginDir, action);
- continue;
- }
+ query = query.Where(p => !excludeStatuses.Contains(p.Status ?? PluginStatus.Indeterminate));
+ }
- if (dirName.Trim().Equals("disable", StringComparison.OrdinalIgnoreCase) || dirName.Trim().Equals("disabled", StringComparison.OrdinalIgnoreCase) || dirName.Trim().Equals("_", StringComparison.OrdinalIgnoreCase))
- {
- continue;
- }
+ return query.FirstOrDefault(p =>
+ p.PluginDirectory?.Trim().Equals(directory.Trim(), StringComparison.OrdinalIgnoreCase) ?? false);
+ }
+
+ private PluginLoader CreatePluginLoader( string entrypointDll )
+ {
+ var currentContext = AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly());
- if (dirName.Trim().Length >= 2 && dirName.StartsWith('_'))
+ return PluginLoader.CreateFromAssemblyFile(
+ entrypointDll,
+ [typeof(BasePlugin), .. _sharedTypes],
+ config =>
{
- continue;
- }
+ config.IsUnloadable = true;
+ config.LoadInMemory = true;
- action(pluginDir);
- }
+ if (currentContext != null)
+ {
+ config.DefaultContext = currentContext;
+ config.PreferSharedTypes = true;
+ }
+ });
}
- public bool LoadPlugin( string pluginId, bool silent = false )
+ private static Type? FindPluginType( PluginLoader loader )
{
- return LoadPluginById(pluginId, silent);
+ return loader.LoadDefaultAssembly()
+ .GetTypes()
+ .FirstOrDefault(t => t.IsSubclassOf(typeof(BasePlugin)));
}
- public bool UnloadPlugin( string pluginId, bool silent = false )
+ private SwiftlyCore CreateSwiftlyCore( PluginMetadata metadata, Type pluginType, string pluginDirectory )
{
- return UnloadPluginById(pluginId, silent);
+ var dataDir = _dataDirectoryService.GetPluginDataDirectory(metadata.Id);
+ var core = new SwiftlyCore(
+ metadata.Id,
+ pluginDirectory,
+ metadata,
+ pluginType,
+ _rootProvider,
+ dataDir);
+
+ core.InitializeType(pluginType);
+ return core;
}
- public bool ReloadPlugin( string pluginId, bool silent = false )
+ private static BasePlugin InstantiatePlugin( Type pluginType, SwiftlyCore core )
{
- return ReloadPluginById(pluginId, silent);
+ var plugin = (BasePlugin)Activator.CreateInstance(pluginType, [core])!;
+ core.InitializeObject(plugin);
+ return plugin;
}
- public PluginStatus? GetPluginStatus( string pluginId )
+ private void CleanupFailedPlugin( BasePlugin? plugin, PluginLoader? loader, SwiftlyCore? core )
{
- foreach (var plugin in plugins)
+ try
{
- if (plugin.Metadata != null && plugin.Metadata.Id == pluginId)
+ plugin?.Unload();
+ loader?.Dispose();
+ core?.Dispose();
+ }
+ catch (Exception ex)
+ {
+ if (GlobalExceptionHandler.Handle(ex))
{
- return plugin.Status;
+ AnsiConsole.WriteException(ex);
}
}
- return null;
}
- public PluginMetadata? GetPluginMetadata( string pluginId )
+ private PluginContext? FailWithError( PluginContext context, bool silent, string message )
{
- foreach (var plugin in plugins)
+ if (!silent) _logger.LogWarning("{Message}", message);
+ context.Status = PluginStatus.Error;
+
+ if (!string.IsNullOrWhiteSpace(context.PluginDirectory))
{
- if (plugin.Metadata != null && plugin.Metadata.Id == pluginId)
- {
- return plugin.Metadata;
- }
+ var pluginName = Path.GetFileName(context.PluginDirectory);
+ _pluginLoadErrors[pluginName] = message;
}
+
return null;
}
- public string? GetPluginPath( string pluginId )
+ private void RebuildSharedServices()
{
- foreach (var plugin in plugins)
+ _interfaceManager.Dispose();
+
+ var loadedPlugins = _plugins
+ .Where(p => p.Status == PluginStatus.Loaded && p.Plugin != null)
+ .ToList();
+
+ foreach (var plugin in loadedPlugins)
{
- if (plugin.Metadata != null && plugin.Metadata.Id == pluginId)
- {
- return plugin.PluginDirectory;
- }
+ plugin.Plugin!.ConfigureSharedInterface(_interfaceManager);
}
- return null;
- }
- public Dictionary GetPluginPaths()
- {
- var paths = new Dictionary();
- foreach (var plugin in plugins)
+ _interfaceManager.Build();
+
+ foreach (var plugin in loadedPlugins)
{
- if (plugin.Metadata != null && plugin.PluginDirectory != null)
- {
- paths[plugin.Metadata.Id] = plugin.PluginDirectory;
- }
+ plugin.Plugin!.UseSharedInterface(_interfaceManager);
+ plugin.Plugin!.OnSharedInterfaceInjected(_interfaceManager);
}
- return paths;
}
- public Dictionary GetAllPluginStatuses()
+ private void NotifyAllPluginsLoaded()
{
- var statuses = new Dictionary();
- foreach (var plugin in plugins)
+ foreach (var plugin in _plugins.Where(p => p.Status == PluginStatus.Loaded && p.Plugin != null))
{
- if (plugin.Metadata != null)
- {
- statuses[plugin.Metadata.Id] = plugin.Status ?? PluginStatus.Indeterminate;
- }
+ plugin.Plugin!.OnAllPluginsLoaded();
}
- return statuses;
}
- public Dictionary GetAllPluginMetadata()
+ private static void EnumeratePluginDirectories( string directory, Action action )
{
- var metadatas = new Dictionary();
- foreach (var plugin in plugins)
+ foreach (var pluginDir in Directory.GetDirectories(directory))
{
- if (plugin.Metadata != null)
+ var dirName = Path.GetFileName(pluginDir);
+
+ // Handle nested plugin directories (e.g., [category])
+ if (dirName.Trim().StartsWith('[') && dirName.EndsWith(']'))
{
- metadatas[plugin.Metadata.Id] = plugin.Metadata;
+ EnumeratePluginDirectories(pluginDir, action);
+ continue;
}
- }
- return metadatas;
- }
- public IEnumerable GetAllPlugins()
- {
- foreach (var plugin in plugins)
- {
- if (plugin.Metadata != null)
+ // Skip disabled directories
+ if (IsDisabledDirectory(dirName))
{
- yield return plugin.Metadata.Id;
+ continue;
}
+
+ action(pluginDir);
}
}
+
+ private static bool IsDisabledDirectory( string dirName )
+ {
+ var trimmed = dirName.Trim();
+
+ return trimmed.Equals("disable", StringComparison.OrdinalIgnoreCase) ||
+ trimmed.Equals("disabled", StringComparison.OrdinalIgnoreCase) ||
+ trimmed.Equals("_", StringComparison.OrdinalIgnoreCase) ||
+ (trimmed.Length >= 2 && trimmed.StartsWith('_'));
+ }
+
+ private string GetDisplayPath( string pluginDirectory )
+ {
+ var relativePath = Path.GetRelativePath(_rootDirService.GetRoot(), pluginDirectory);
+ return Path.Join("(swRoot)", relativePath);
+ }
+
+ private static string GetPluginEntrypointPath( string directory )
+ {
+ var dirName = Path.GetFileName(directory);
+ return Path.Combine(directory, $"{dirName}.dll");
+ }
+
+ private void LogPluginLoadSuccess( PluginContext context, string displayPath )
+ {
+ _logger.LogInformation(
+ string.Join("\n", [
+ "Loaded Plugin",
+ "├─ {Id} {Version}",
+ "├─ Author: {Author}",
+ "└─ Path: {RelativePath}"
+ ]),
+ context.Metadata!.Id,
+ context.Metadata!.Version,
+ context.Metadata!.Author,
+ displayPath);
+ }
+}
+
+internal static class StringExtensions
+{
+ public static string TrimEnd( this string source, string suffix, StringComparison comparison )
+ {
+ return source.EndsWith(suffix, comparison)
+ ? source[..^suffix.Length]
+ : source;
+ }
}
\ No newline at end of file
diff --git a/managed/src/SwiftlyS2.Core/Modules/Plugins/SwiftlyCore.cs b/managed/src/SwiftlyS2.Core/Modules/Plugins/SwiftlyCore.cs
index 22cb4ad98..b241e3bd5 100644
--- a/managed/src/SwiftlyS2.Core/Modules/Plugins/SwiftlyCore.cs
+++ b/managed/src/SwiftlyS2.Core/Modules/Plugins/SwiftlyCore.cs
@@ -76,8 +76,6 @@ internal class SwiftlyCore : ISwiftlyCore, IDisposable
public Localizer Localizer { get; init; }
public PermissionManager PermissionManager { get; init; }
public RegistratorService RegistratorService { get; init; }
- // [Obsolete("MenuManager will be deprecared at the release of SwiftlyS2. Please use MenuManagerAPI instead")]
- // public MenuManager MenuManager { get; init; }
public MenuManagerAPI MenuManagerAPI { get; init; }
public CommandLineService CommandLineService { get; init; }
public HelpersService Helpers { get; init; }
@@ -126,7 +124,7 @@ public SwiftlyCore( string contextId, string contextBaseDirectory, PluginMetadat
.AddSingleton()
.AddSingleton()
.AddSingleton()
- .AddSingleton(provider => provider.GetRequiredService().GetLocalizer())
+ .AddSingleton(provider => provider.GetRequiredService().GetLocalizer())
.AddSingleton()
// .AddSingleton()
.AddSingleton()
@@ -235,11 +233,9 @@ public void Dispose()
ISchedulerService ISwiftlyCore.Scheduler => SchedulerService;
IDatabaseService ISwiftlyCore.Database => DatabaseService;
ITranslationService ISwiftlyCore.Translation => TranslationService;
- ILocalizer ISwiftlyCore.Localizer => Localizer;
+ ILocalizer ISwiftlyCore.Localizer => TranslationService.GetLocalizer();
IPermissionManager ISwiftlyCore.Permission => PermissionManager;
IRegistratorService ISwiftlyCore.Registrator => RegistratorService;
- // [Obsolete("MenuManager will be deprecared at the release of SwiftlyS2. Please use MenuManagerAPI instead")]
- // IMenuManager ISwiftlyCore.Menus => MenuManager;
IMenuManagerAPI ISwiftlyCore.MenusAPI => MenuManagerAPI;
ICommandLine ISwiftlyCore.CommandLine => CommandLineService;
IHelpers ISwiftlyCore.Helpers => Helpers;
@@ -252,4 +248,5 @@ public void Dispose()
string ISwiftlyCore.PluginDataDirectory => PluginDataDirectory;
string ISwiftlyCore.CSGODirectory => NativeEngineHelpers.GetCSGODirectoryPath();
string ISwiftlyCore.GameDirectory => NativeEngineHelpers.GetGameDirectoryPath();
+ bool ISwiftlyCore.IsGameThread => NativeCore.IsMainThread();
}
\ No newline at end of file
diff --git a/managed/src/SwiftlyS2.Core/Modules/Scheduler/SchedulerManager.cs b/managed/src/SwiftlyS2.Core/Modules/Scheduler/SchedulerManager.cs
index b9c51dab8..dffa7c8b6 100644
--- a/managed/src/SwiftlyS2.Core/Modules/Scheduler/SchedulerManager.cs
+++ b/managed/src/SwiftlyS2.Core/Modules/Scheduler/SchedulerManager.cs
@@ -2,9 +2,6 @@
using Spectre.Console;
using SwiftlyS2.Core.Natives;
using SwiftlyS2.Shared.Scheduler;
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading;
namespace SwiftlyS2.Core.Scheduler;
@@ -22,27 +19,34 @@ internal static class SchedulerManager
private static long _currentTick = 0;
private static long _currentTimeMs = 0;
- private static readonly ConcurrentQueue _asyncOnTickTaskQueue = new();
- private static readonly ConcurrentQueue _asyncOnWorldUpdateTaskQueue = new();
+ private static readonly ConcurrentQueue _asyncOnTickTaskQueue = [];
+ private static readonly ConcurrentQueue _asyncOnWorldUpdateTaskQueue = [];
// Min-heap keyed by DueTick
private static readonly PriorityQueue _timerQueue = new();
private static readonly PriorityQueue _timerQueueMs = new();
// Next-tick tasks keyed by guid so services can remove them before they run
- private static readonly List<(Action action, CancellationToken ownerToken)> _nextTickTasks = new();
+ private static readonly List<(Action action, CancellationToken ownerToken)> _nextTickTasks = [];
- private static readonly List<(Action action, CancellationToken ownerToken)> _nextWorldUpdateTasks = new();
+ private static readonly List<(Action action, CancellationToken ownerToken)> _nextWorldUpdateTasks = [];
public static void OnWorldUpdate()
{
- ExecuteOnWorldUpdateAsyncTasks();
- ExecuteOnWorldUpdateTimers();
+ try
+ {
+ ExecuteOnWorldUpdateAsyncTasks();
+ ExecuteOnWorldUpdateTimers();
+ }
+ catch (Exception ex)
+ {
+ if (GlobalExceptionHandler.Handle(ex)) AnsiConsole.WriteException(ex);
+ }
}
private static void ExecuteOnWorldUpdateAsyncTasks()
{
- int batchCount = _asyncOnWorldUpdateTaskQueue.Count;
+ var batchCount = _asyncOnWorldUpdateTaskQueue.Count;
while (batchCount > 0 && _asyncOnWorldUpdateTaskQueue.TryDequeue(out var task))
{
batchCount--;
@@ -89,13 +93,20 @@ private static void ExecuteOnWorldUpdateTimers()
public static void OnTick()
{
_currentTimeMs = DateTimeOffset.Now.ToUnixTimeMilliseconds();
- ExecuteOnTickAsyncTasks();
- ExecuteOnTickTimers();
+ try
+ {
+ ExecuteOnTickAsyncTasks();
+ ExecuteOnTickTimers();
+ }
+ catch (Exception ex)
+ {
+ if (GlobalExceptionHandler.Handle(ex)) AnsiConsole.WriteException(ex);
+ }
}
private static void ExecuteOnTickAsyncTasks()
{
- int batchCount = _asyncOnTickTaskQueue.Count;
+ var batchCount = _asyncOnTickTaskQueue.Count;
while (batchCount > 0 && _asyncOnTickTaskQueue.TryDequeue(out var task))
{
batchCount--;
@@ -105,7 +116,7 @@ private static void ExecuteOnTickAsyncTasks()
}
catch (Exception ex)
{
- AnsiConsole.WriteException(ex);
+ if (GlobalExceptionHandler.Handle(ex)) AnsiConsole.WriteException(ex);
}
}
}
@@ -128,7 +139,7 @@ private static void ExecuteOnTickTimers()
{
if (!_timerQueue.TryPeek(out var timer, out var due)) break;
if (due > _currentTick) break;
- _timerQueue.Dequeue();
+ _ = _timerQueue.Dequeue();
// Skip canceled/owner-disposed timers
if (timer.CancellationTokenSource.IsCancellationRequested || timer.OwnerToken.IsCancellationRequested)
@@ -143,7 +154,7 @@ private static void ExecuteOnTickTimers()
{
if (!_timerQueueMs.TryPeek(out var timer, out var due)) break;
if (due > _currentTimeMs) break;
- _timerQueueMs.Dequeue();
+ _ = _timerQueueMs.Dequeue();
// Skip canceled/owner-disposed timers
if (timer.CancellationTokenSource.IsCancellationRequested || timer.OwnerToken.IsCancellationRequested)
@@ -167,8 +178,7 @@ private static void ExecuteOnTickTimers()
}
catch (Exception ex)
{
- if (!GlobalExceptionHandler.Handle(ex)) return;
- AnsiConsole.WriteException(ex);
+ if (GlobalExceptionHandler.Handle(ex)) AnsiConsole.WriteException(ex);
}
}
}
@@ -184,8 +194,7 @@ private static void ExecuteOnTickTimers()
}
catch (Exception ex)
{
- if (!GlobalExceptionHandler.Handle(ex)) return;
- AnsiConsole.WriteException(ex);
+ if (GlobalExceptionHandler.Handle(ex)) AnsiConsole.WriteException(ex);
}
}
}
@@ -213,6 +222,7 @@ private static void ExecuteTimer( Timer timer )
_timerQueueMs.Enqueue(timer, timer.Context.ExpectedNextTimeMs);
break;
case TimerStep.StopStep:
+ if (timer.CancellationTokenSource.IsCancellationRequested) break;
timer.CancellationTokenSource.Cancel();
break;
default:
@@ -241,7 +251,8 @@ public static CancellationTokenSource AddTimer( Func ta
var cancellationTokenSource = new CancellationTokenSource();
- var _ = NextTickAsync(() => {
+ var _ = NextTickAsync(() =>
+ {
var timer = new Timer {
Task = task,
CancellationTokenSource = cancellationTokenSource,
@@ -382,11 +393,6 @@ public static Task QueueOrNow( Action action )
public static Task QueueOrNow( Func task )
{
- if (NativeBinding.IsMainThread)
- {
- return Task.FromResult(task());
- }
-
- return NextWorldUpdateAsync(task);
+ return NativeBinding.IsMainThread ? Task.FromResult(task()) : NextWorldUpdateAsync(task);
}
}
\ No newline at end of file
diff --git a/managed/src/SwiftlyS2.Core/Modules/Scheduler/SchedulerService.cs b/managed/src/SwiftlyS2.Core/Modules/Scheduler/SchedulerService.cs
index 065d71e69..e0957f670 100644
--- a/managed/src/SwiftlyS2.Core/Modules/Scheduler/SchedulerService.cs
+++ b/managed/src/SwiftlyS2.Core/Modules/Scheduler/SchedulerService.cs
@@ -5,7 +5,7 @@ namespace SwiftlyS2.Core.Scheduler;
internal class SchedulerService : ISchedulerService, IDisposable
{
- private readonly List _timers = new();
+ private readonly List _timers = [];
private readonly Lock _lock = new();
private readonly CancellationTokenSource _lifecycleCts = new();
private CancellationTokenSource _mapChangeCts = new();
@@ -175,14 +175,14 @@ public CancellationTokenSource AddTimer( Func task )
public void StopOnMapChange( CancellationTokenSource cts )
{
- _mapChangeCts.Token.Register(cts.Cancel);
+ _ = _mapChangeCts.Token.Register(cts.Cancel);
}
private void CleanFinishedTimers()
{
lock (_lock)
{
- _timers.RemoveAll(timer => timer.IsCancellationRequested);
+ _ = _timers.RemoveAll(timer => timer.IsCancellationRequested);
}
}
diff --git a/managed/src/SwiftlyS2.Core/Modules/Schemas/Extensions/CBaseModelEntity.cs b/managed/src/SwiftlyS2.Core/Modules/Schemas/Extensions/CBaseModelEntity.cs
index 1259d8aab..47cab8622 100644
--- a/managed/src/SwiftlyS2.Core/Modules/Schemas/Extensions/CBaseModelEntity.cs
+++ b/managed/src/SwiftlyS2.Core/Modules/Schemas/Extensions/CBaseModelEntity.cs
@@ -4,6 +4,30 @@ namespace SwiftlyS2.Shared.SchemaDefinitions;
public partial interface CBaseModelEntity
{
+ ///
+ /// Gets the skeletance instance of the entity.
+ ///
+ ///
+ public CSkeletonInstance? GetSkeletonInstance();
+
+ ///
+ /// Gets the model of the entity.
+ ///
+ ///
+ public string? GetModel();
+
+ ///
+ /// Gets the model meshgroupmask of the entity.
+ ///
+ ///
+ public ulong? GetMeshGroupMask();
+
+ ///
+ /// Sets the model meshgroupmask of the entity.
+ ///
+ ///
+ public void SetMeshGroupMask(ulong meshGroupMask);
+
///
/// Sets the model to the entity.
///
@@ -11,13 +35,13 @@ public partial interface CBaseModelEntity
///
/// The model path to be used.
[ThreadUnsafe]
- public void SetModel( string model );
+ public void SetModel(string model);
///
/// Sets the model to the entity asynchronously.
///
/// The model path to be used.
- public Task SetModelAsync( string model );
+ public Task SetModelAsync(string model);
///
/// Sets the bodygroup to the entity.
@@ -25,14 +49,14 @@ public partial interface CBaseModelEntity
/// Thread unsafe, use async variant instead for non-main thread context.
///
[ThreadUnsafe]
- public void SetBodygroupByName( string group, int value );
+ public void SetBodygroupByName(string group, int value);
///
/// Sets the bodygroup to the entity asynchronously.
///
/// The name of the bodygroup to be set.
/// The value to be set for the bodygroup.
- public Task SetBodygroupByNameAsync( string group, int value );
+ public Task SetBodygroupByNameAsync(string group, int value);
///
/// Sets the scale of the entity.
@@ -40,10 +64,10 @@ public partial interface CBaseModelEntity
/// Thread unsafe, use async variant instead for non-main thread context.
///
[ThreadUnsafe]
- public void SetScale( float scale );
+ public void SetScale(float scale);
///
/// Sets the scale of the entity.
///
- public Task SetScaleAsync( float scale );
+ public Task SetScaleAsync(float scale);
}
\ No newline at end of file
diff --git a/managed/src/SwiftlyS2.Core/Modules/Schemas/Extensions/CBaseModelEntityImpl.cs b/managed/src/SwiftlyS2.Core/Modules/Schemas/Extensions/CBaseModelEntityImpl.cs
index 3d51fa17a..9d4d0d5ae 100644
--- a/managed/src/SwiftlyS2.Core/Modules/Schemas/Extensions/CBaseModelEntityImpl.cs
+++ b/managed/src/SwiftlyS2.Core/Modules/Schemas/Extensions/CBaseModelEntityImpl.cs
@@ -6,31 +6,55 @@ namespace SwiftlyS2.Core.SchemaDefinitions;
internal partial class CBaseModelEntityImpl : CBaseModelEntity
{
- public void SetModel( string model )
+ public CSkeletonInstance? GetSkeletonInstance()
+ {
+ return CBodyComponent?.SceneNode?.GetSkeletonInstance();
+ }
+
+ public string? GetModel()
+ {
+ return GetSkeletonInstance()?.ModelState.ModelName;
+ }
+
+ public void SetModel(string model)
{
NativeBinding.ThrowIfNonMainThread();
GameFunctions.SetModel(Address, model);
}
- public Task SetModelAsync( string model )
+ public Task SetModelAsync(string model)
{
return SchedulerManager.QueueOrNow(() => SetModel(model));
}
- public void SetBodygroupByName( string group, int value )
+ public void SetBodygroupByName(string group, int value)
{
NativeBinding.ThrowIfNonMainThread();
AcceptInput("SetBodygroup", $"{group},{value}");
}
- public Task SetBodygroupByNameAsync( string group, int value )
+ public Task SetBodygroupByNameAsync(string group, int value)
{
return SchedulerManager.QueueOrNow(() => SetBodygroupByName(group, value));
}
- public void SetScale( float scale )
+ public ulong? GetMeshGroupMask()
+ {
+ return GetSkeletonInstance()?.ModelState.MeshGroupMask;
+ }
+
+ public void SetMeshGroupMask(ulong meshGroupMask)
+ {
+ var skeletonInstance = GetSkeletonInstance();
+ if (skeletonInstance == null) return;
+
+ skeletonInstance.ModelState.MeshGroupMask = meshGroupMask;
+ CBodyComponentUpdated();
+ }
+
+ public void SetScale(float scale)
{
- var skeletonInstance = CBodyComponent?.SceneNode?.GetSkeletonInstance();
+ var skeletonInstance = GetSkeletonInstance();
if (skeletonInstance == null) return;
skeletonInstance.Scale = scale;
@@ -38,17 +62,17 @@ public void SetScale( float scale )
CBodyComponentUpdated();
}
- public Task SetScaleAsync( float scale )
+ public Task SetScaleAsync(float scale)
{
return SchedulerManager.QueueOrNow(() => SetScale(scale));
}
- public void ChangeSubclass( ushort itemDefinitionIndex )
+ public void ChangeSubclass(ushort itemDefinitionIndex)
{
AcceptInput("ChangeSubclass", itemDefinitionIndex.ToString());
}
- public Task ChangeSubclassAsync( ushort itemDefinitionIndex )
+ public Task ChangeSubclassAsync(ushort itemDefinitionIndex)
{
return SchedulerManager.QueueOrNow(() => ChangeSubclass(itemDefinitionIndex));
}
diff --git a/managed/src/SwiftlyS2.Core/Modules/Schemas/Extensions/CBasePlayerControllerImpl.cs b/managed/src/SwiftlyS2.Core/Modules/Schemas/Extensions/CBasePlayerControllerImpl.cs
index 69a086a1c..67d58da7b 100644
--- a/managed/src/SwiftlyS2.Core/Modules/Schemas/Extensions/CBasePlayerControllerImpl.cs
+++ b/managed/src/SwiftlyS2.Core/Modules/Schemas/Extensions/CBasePlayerControllerImpl.cs
@@ -9,15 +9,15 @@ internal partial class CBasePlayerControllerImpl : CBasePlayerController
{
public void SetPawn( CBasePlayerPawn? pawn )
{
- nint? handle = pawn?.Address;
+ var handle = pawn?.Address;
GameFunctions.SetPlayerControllerPawn(Address, handle ?? IntPtr.Zero, true, false, false, false);
}
public IPlayer? ToPlayer()
{
if (!IsValid) return null;
- var player = PlayerManagerService.PlayerObjects[(int)(Index - 1)];
- if (player is not { IsValid: true } || !NativePlayerManager.IsPlayerOnline(player.PlayerID)) return null;
- return player;
+ if (!PlayerManagerService.PlayerObjects.TryGetValue((int)(Index - 1), out var player)) return null;
+
+ return player is { IsValid: true } ? player : null;
}
}
\ No newline at end of file
diff --git a/managed/src/SwiftlyS2.Core/Modules/Schemas/Extensions/CEntityInstance.cs b/managed/src/SwiftlyS2.Core/Modules/Schemas/Extensions/CEntityInstance.cs
index c65bb05fe..78db1a692 100644
--- a/managed/src/SwiftlyS2.Core/Modules/Schemas/Extensions/CEntityInstance.cs
+++ b/managed/src/SwiftlyS2.Core/Modules/Schemas/Extensions/CEntityInstance.cs
@@ -1,4 +1,3 @@
-using SwiftlyS2.Core.EntitySystem;
using SwiftlyS2.Shared.EntitySystem;
using SwiftlyS2.Shared.Misc;
@@ -6,7 +5,10 @@ namespace SwiftlyS2.Shared.SchemaDefinitions;
public partial interface CEntityInstance : IEquatable
{
-
+ ///
+ /// Whether the entity instance is valid.
+ ///
+ public new bool IsValid { get; }
///
/// The index of the entity.
diff --git a/managed/src/SwiftlyS2.Core/Modules/Schemas/Extensions/CEntityInstanceImpl.cs b/managed/src/SwiftlyS2.Core/Modules/Schemas/Extensions/CEntityInstanceImpl.cs
index 073058581..03fc596b4 100644
--- a/managed/src/SwiftlyS2.Core/Modules/Schemas/Extensions/CEntityInstanceImpl.cs
+++ b/managed/src/SwiftlyS2.Core/Modules/Schemas/Extensions/CEntityInstanceImpl.cs
@@ -3,6 +3,7 @@
using SwiftlyS2.Shared.Natives;
using SwiftlyS2.Shared.EntitySystem;
using SwiftlyS2.Shared.SchemaDefinitions;
+using SwiftlyS2.Core.EntitySystem;
namespace SwiftlyS2.Core.SchemaDefinitions;
@@ -11,11 +12,22 @@ internal partial class CEntityInstanceImpl : CEntityInstance, IEquatable Entity?.EntityHandle.EntityIndex ?? uint.MaxValue;
public string DesignerName => Entity?.DesignerName ?? string.Empty;
- public bool IsValidEntity => NativeEntitySystem.IsValidEntity(Address);
+ public new bool IsValid => base.IsValid && IsValidEntity;
+
+ public bool IsValidEntity => EntityManager.IsAddressValid(Address);
+
+ private void ThrowIfInvalidEntity()
+ {
+ if (!IsValidEntity)
+ {
+ throw new InvalidOperationException("The entity instance is no longer valid.");
+ }
+ }
public unsafe void AcceptInput( string input, T? value, CEntityInstance? activator = null, CEntityInstance? caller = null, int outputID = 0 )
{
NativeBinding.ThrowIfNonMainThread();
+ ThrowIfInvalidEntity();
using var variant = new CVariant(value);
@@ -30,6 +42,7 @@ public Task AcceptInputAsync( string input, T? value, CEntityInstance? activa
public unsafe void AddEntityIOEvent( string input, T? value, CEntityInstance? activator = null, CEntityInstance? caller = null, float delay = 0f )
{
NativeBinding.ThrowIfNonMainThread();
+ ThrowIfInvalidEntity();
using var variant = new CVariant