diff --git a/source/E2E.Tests/Services/BasicCharacterObjects/BasicCharacterObjectSyncTests.cs b/source/E2E.Tests/Services/BasicCharacterObjects/BasicCharacterObjectSyncTests.cs index ccf5e913d..e500e276f 100644 --- a/source/E2E.Tests/Services/BasicCharacterObjects/BasicCharacterObjectSyncTests.cs +++ b/source/E2E.Tests/Services/BasicCharacterObjects/BasicCharacterObjectSyncTests.cs @@ -1,6 +1,9 @@ using E2E.Tests.Environment; using E2E.Tests.Environment.Instance; +using HarmonyLib; +using TaleWorlds.CampaignSystem.Settlements; using TaleWorlds.Core; +using TaleWorlds.Localization; using Xunit.Abstractions; namespace E2E.Tests.Services.BasicCharacterObjects @@ -24,16 +27,37 @@ public void ServerBasicCharacterObject_SyncAll() // Arrange var server = TestEnvironment.Server; + var basicHeroField = AccessTools.Field(typeof(BasicCharacterObject), nameof(BasicCharacterObject._isBasicHero)); + var mountedField = AccessTools.Field(typeof(BasicCharacterObject), nameof(BasicCharacterObject._isMounted)); + var rangedField = AccessTools.Field(typeof(BasicCharacterObject), nameof(BasicCharacterObject._isRanged)); + var rosterField = AccessTools.Field(typeof(BasicCharacterObject), nameof(BasicCharacterObject._equipmentRoster)); + var skillField = AccessTools.Field(typeof(BasicCharacterObject), nameof(BasicCharacterObject.DefaultCharacterSkills)); + var nameField = AccessTools.Field(typeof(BasicCharacterObject), nameof(BasicCharacterObject._basicName)); + // Get field intercept to use on the server to simulate the field changing + var heroIntercept = TestEnvironment.GetIntercept(basicHeroField); + var mountedIntercept = TestEnvironment.GetIntercept(mountedField); + var rangedIntercept = TestEnvironment.GetIntercept(rangedField); + var rosterIntercept = TestEnvironment.GetIntercept(rosterField); + var skillIntercept = TestEnvironment.GetIntercept(skillField); + var nameIntercept = TestEnvironment.GetIntercept(nameField); + // Act string? characterId = null; string? cultureId = null; + string? skillId = null; + string? equipmentRosterId = null; + TextObject name = new TextObject("test"); server.Call(() => { BasicCharacterObject characterObject = new BasicCharacterObject(); BasicCultureObject culture = new BasicCultureObject(); + MBCharacterSkills skills = new MBCharacterSkills(); + MBEquipmentRoster equipmentRoster = new MBEquipmentRoster(); Assert.True(server.ObjectManager.TryGetId(characterObject, out characterId)); Assert.True(server.ObjectManager.TryGetId(culture, out cultureId)); + Assert.True(server.ObjectManager.TryGetId(skills, out skillId)); + Assert.True(server.ObjectManager.TryGetId(equipmentRoster, out equipmentRosterId)); characterObject.Age = 5; characterObject.BeardTags = "test"; @@ -54,8 +78,19 @@ public void ServerBasicCharacterObject_SyncAll() characterObject.Race = 4; characterObject.TattooTags = "test"; + // Simulate the field changing + heroIntercept.Invoke(null, new object[] { characterObject, true }); + mountedIntercept.Invoke(null, new object[] { characterObject, true }); + rangedIntercept.Invoke(null, new object[] { characterObject, true }); + rosterIntercept.Invoke(null, new object[] { characterObject, equipmentRoster }); + skillIntercept.Invoke(null, new object[] { characterObject, skills }); + nameIntercept.Invoke(null, new object[] { characterObject, name }); }); + Assert.True(server.ObjectManager.TryGetObject(skillId, out MBCharacterSkills serverSkills)); + Assert.True(server.ObjectManager.TryGetObject(equipmentRosterId, out MBEquipmentRoster serverEquipmentRoster)); + Assert.True(server.ObjectManager.TryGetObject(cultureId, out BasicCultureObject serverCulture)); + foreach (var client in TestEnvironment.Clients) { Assert.True(client.ObjectManager.TryGetObject(characterId, out BasicCharacterObject clientCharacter)); @@ -66,7 +101,7 @@ public void ServerBasicCharacterObject_SyncAll() Assert.Equal(FormationClass.Cavalry, clientCharacter.DefaultFormationClass); Assert.Equal(69, clientCharacter.DefaultFormationGroup); Assert.Equal(420, clientCharacter.DismountResistance); - Assert.Equal(clientCulture, clientCharacter.Culture); //Basic Culture Object lifetime + Assert.Equal(serverCulture.StringId, clientCharacter.Culture.StringId); //Basic Culture Object lifetime Assert.Equal(42, clientCharacter.FaceDirtAmount); Assert.True(clientCharacter.FaceMeshCache); Assert.Equal(FormationPositionPreference.Middle, clientCharacter.FormationPositionPreference); @@ -79,6 +114,13 @@ public void ServerBasicCharacterObject_SyncAll() Assert.Equal(66, clientCharacter.Level); Assert.Equal(4, clientCharacter.Race); Assert.Equal("test", clientCharacter.TattooTags); + + Assert.True(clientCharacter._isBasicHero); + Assert.True(clientCharacter._isMounted); + Assert.True(clientCharacter._isRanged); + Assert.Equal(serverEquipmentRoster.StringId, clientCharacter._equipmentRoster.StringId); + Assert.Equal(serverSkills.StringId, clientCharacter.DefaultCharacterSkills.StringId); + Assert.True(name.Equals(clientCharacter._basicName)); } } } diff --git a/source/GameInterface/Services/BasicCharacterObjects/BasicCharacterObjectSync.cs b/source/GameInterface/Services/BasicCharacterObjects/BasicCharacterObjectSync.cs index 5a36d5036..f73f2247c 100644 --- a/source/GameInterface/Services/BasicCharacterObjects/BasicCharacterObjectSync.cs +++ b/source/GameInterface/Services/BasicCharacterObjects/BasicCharacterObjectSync.cs @@ -27,6 +27,13 @@ public BasicCharacterObjectSync(IAutoSyncBuilder autoSyncBuilder) autoSyncBuilder.AddProperty(AccessTools.Property(typeof(BasicCharacterObject), nameof(BasicCharacterObject.Level))); autoSyncBuilder.AddProperty(AccessTools.Property(typeof(BasicCharacterObject), nameof(BasicCharacterObject.Race))); autoSyncBuilder.AddProperty(AccessTools.Property(typeof(BasicCharacterObject), nameof(BasicCharacterObject.TattooTags))); + + autoSyncBuilder.AddField(AccessTools.Field(typeof(BasicCharacterObject), nameof(BasicCharacterObject._isBasicHero))); + autoSyncBuilder.AddField(AccessTools.Field(typeof(BasicCharacterObject), nameof(BasicCharacterObject._isMounted))); + autoSyncBuilder.AddField(AccessTools.Field(typeof(BasicCharacterObject), nameof(BasicCharacterObject._isRanged))); + autoSyncBuilder.AddField(AccessTools.Field(typeof(BasicCharacterObject), nameof(BasicCharacterObject._equipmentRoster))); + autoSyncBuilder.AddField(AccessTools.Field(typeof(BasicCharacterObject), nameof(BasicCharacterObject.DefaultCharacterSkills))); + autoSyncBuilder.AddField(AccessTools.Field(typeof(BasicCharacterObject), nameof(BasicCharacterObject._basicName))); } } } diff --git a/source/GameInterface/Services/CharacterSkills/CharacterSkillsRegistry.cs b/source/GameInterface/Services/CharacterSkills/CharacterSkillsRegistry.cs new file mode 100644 index 000000000..465c5c792 --- /dev/null +++ b/source/GameInterface/Services/CharacterSkills/CharacterSkillsRegistry.cs @@ -0,0 +1,38 @@ +using GameInterface.Services.Registry; +using System.Threading; +using TaleWorlds.Core; +using TaleWorlds.ObjectSystem; + +namespace GameInterface.Services.CharacterSkills +{ + internal class CharacterSkillsRegistry : RegistryBase + { + private const string IdPrefix = "CoopCharacterSkills"; + private static int InstanceCounter = 0; + + public CharacterSkillsRegistry(IRegistryCollection collection) : base(collection) + { + } + + public override void RegisterAll() + { + var objectManager = MBObjectManager.Instance; + + if (objectManager == null) + { + Logger.Error("Unable to register objects when CampaignObjectManager is null"); + return; + } + + foreach (var skill in objectManager.GetObjectTypeList()) + { + RegisterExistingObject(skill.StringId, skill); + } + } + + protected override string GetNewId(MBCharacterSkills obj) + { + return $"{IdPrefix}_{Interlocked.Increment(ref InstanceCounter)}"; + } + } +} diff --git a/source/GameInterface/Services/CharacterSkills/Handlers/CharacterSkillsLifetimeHandler.cs b/source/GameInterface/Services/CharacterSkills/Handlers/CharacterSkillsLifetimeHandler.cs new file mode 100644 index 000000000..73f3dc2b7 --- /dev/null +++ b/source/GameInterface/Services/CharacterSkills/Handlers/CharacterSkillsLifetimeHandler.cs @@ -0,0 +1,56 @@ +using Common.Logging; +using Common.Messaging; +using Common.Network; +using Common.Util; +using GameInterface.Services.CharacterSkills.Messages; +using GameInterface.Services.ObjectManager; +using Serilog; +using TaleWorlds.Core; + +namespace GameInterface.Services.CharacterSkills.Handlers +{ + internal class CharacterSkillsLifetimeHandler : IHandler + { + private static readonly ILogger Logger = LogManager.GetLogger(); + private readonly IMessageBroker messageBroker; + private readonly IObjectManager objectManager; + private readonly INetwork network; + + public CharacterSkillsLifetimeHandler(IMessageBroker messageBroker, IObjectManager objectManager, INetwork network) + { + this.messageBroker = messageBroker; + this.objectManager = objectManager; + this.network = network; + messageBroker.Subscribe(Handle); + messageBroker.Subscribe(Handle); + } + + public void Dispose() + { + messageBroker.Unsubscribe(Handle); + messageBroker.Unsubscribe(Handle); + } + + private void Handle(MessagePayload obj) + { + var payload = obj.What; + + if (objectManager.AddNewObject(payload.CharacterSkills, out string CharacterSkillsId) == false) return; + + var message = new NetworkCreateCharacterSkills(CharacterSkillsId); + network.SendAll(message); + } + + private void Handle(MessagePayload obj) + { + var payload = obj.What; + + var CharacterSkills = ObjectHelper.SkipConstructor(); + if (objectManager.AddExisting(payload.CharacterSkillsId, CharacterSkills) == false) + { + Logger.Error("Failed to add existing CharacterSkill, {id}", payload.CharacterSkillsId); + return; + } + } + } +} \ No newline at end of file diff --git a/source/GameInterface/Services/CharacterSkills/Messages/CharacterSkillsCreated.cs b/source/GameInterface/Services/CharacterSkills/Messages/CharacterSkillsCreated.cs new file mode 100644 index 000000000..8da842e6d --- /dev/null +++ b/source/GameInterface/Services/CharacterSkills/Messages/CharacterSkillsCreated.cs @@ -0,0 +1,15 @@ +using Common.Messaging; +using TaleWorlds.Core; + +namespace GameInterface.Services.CharacterSkills.Messages +{ + internal class CharacterSkillsCreated : IEvent + { + public MBCharacterSkills CharacterSkills { get; } + + public CharacterSkillsCreated(MBCharacterSkills characterSkills) + { + CharacterSkills = characterSkills; + } + } +} diff --git a/source/GameInterface/Services/CharacterSkills/Messages/NetworkCreateCharacterSkills.cs b/source/GameInterface/Services/CharacterSkills/Messages/NetworkCreateCharacterSkills.cs new file mode 100644 index 000000000..30de3ee76 --- /dev/null +++ b/source/GameInterface/Services/CharacterSkills/Messages/NetworkCreateCharacterSkills.cs @@ -0,0 +1,16 @@ +using Common.Messaging; +using ProtoBuf; + +namespace GameInterface.Services.CharacterSkills.Messages +{ + [ProtoContract(SkipConstructor = true)] + internal class NetworkCreateCharacterSkills : ICommand + { + [ProtoMember(1)] + public string CharacterSkillsId; + public NetworkCreateCharacterSkills(string characterSkillsId) + { + CharacterSkillsId = characterSkillsId; + } + } +} diff --git a/source/GameInterface/Services/CharacterSkills/Patches/CharacterSkillsLifetimePatches.cs b/source/GameInterface/Services/CharacterSkills/Patches/CharacterSkillsLifetimePatches.cs new file mode 100644 index 000000000..dcf8ea2b8 --- /dev/null +++ b/source/GameInterface/Services/CharacterSkills/Patches/CharacterSkillsLifetimePatches.cs @@ -0,0 +1,41 @@ +using Common.Logging; +using Common.Messaging; +using GameInterface.Policies; +using GameInterface.Services.CharacterSkills.Messages; +using HarmonyLib; +using Serilog; +using System; +using TaleWorlds.Core; + +namespace GameInterface.Services.CharacterSkills.Patches +{ + /// + /// Lifetime Patches for CharacterSkills + /// + [HarmonyPatch] + internal class CharacterSkillsLifetimePatches + { + private static ILogger Logger = LogManager.GetLogger(); + + [HarmonyPatch(typeof(MBCharacterSkills), MethodType.Constructor)] + [HarmonyPrefix] + private static bool CreateCharacterSkillsPrefix(ref MBCharacterSkills __instance) + { + // Call original if we call this function + if (CallOriginalPolicy.IsOriginalAllowed()) return true; + + if (ModInformation.IsClient) + { + Logger.Error("Client created unmanaged {name}\n" + + "Callstack: {callstack}", typeof(MBCharacterSkills), Environment.StackTrace); + return false; + } + + var message = new CharacterSkillsCreated(__instance); + + MessageBroker.Instance.Publish(__instance, message); + + return true; + } + } +} diff --git a/source/GameInterface/Services/EquipmentRoster/EquipmentRosterRegistry.cs b/source/GameInterface/Services/EquipmentRoster/EquipmentRosterRegistry.cs new file mode 100644 index 000000000..27be817bc --- /dev/null +++ b/source/GameInterface/Services/EquipmentRoster/EquipmentRosterRegistry.cs @@ -0,0 +1,38 @@ +using GameInterface.Services.Registry; +using System.Threading; +using TaleWorlds.Core; +using TaleWorlds.ObjectSystem; + +namespace GameInterface.Services.EquipmentRoster +{ + internal class EquipmentRosterRegistry : RegistryBase + { + private const string IdPrefix = "CoopEquipmentRoster"; + private static int InstanceCounter = 0; + + public EquipmentRosterRegistry(IRegistryCollection collection) : base(collection) + { + } + + public override void RegisterAll() + { + var objectManager = MBObjectManager.Instance; + + if (objectManager == null) + { + Logger.Error("Unable to register objects when CampaignObjectManager is null"); + return; + } + + foreach (var equipRoster in objectManager.GetObjectTypeList()) + { + RegisterExistingObject(equipRoster.StringId, equipRoster); + } + } + + protected override string GetNewId(MBEquipmentRoster obj) + { + return $"{IdPrefix}_{Interlocked.Increment(ref InstanceCounter)}"; + } + } +} diff --git a/source/GameInterface/Services/EquipmentRoster/Handlers/EquipmentRosterLifetimeHandler.cs b/source/GameInterface/Services/EquipmentRoster/Handlers/EquipmentRosterLifetimeHandler.cs new file mode 100644 index 000000000..50eb776d4 --- /dev/null +++ b/source/GameInterface/Services/EquipmentRoster/Handlers/EquipmentRosterLifetimeHandler.cs @@ -0,0 +1,56 @@ +using Common.Logging; +using Common.Messaging; +using Common.Network; +using Common.Util; +using GameInterface.Services.EquipmentRoster.Messages; +using GameInterface.Services.ObjectManager; +using Serilog; +using TaleWorlds.Core; + +namespace GameInterface.Services.EquipmentRoster.Handlers +{ + internal class EquipmentRosterLifetimeHandler : IHandler + { + private static readonly ILogger Logger = LogManager.GetLogger(); + private readonly IMessageBroker messageBroker; + private readonly IObjectManager objectManager; + private readonly INetwork network; + + public EquipmentRosterLifetimeHandler(IMessageBroker messageBroker, IObjectManager objectManager, INetwork network) + { + this.messageBroker = messageBroker; + this.objectManager = objectManager; + this.network = network; + messageBroker.Subscribe(Handle); + messageBroker.Subscribe(Handle); + } + + public void Dispose() + { + messageBroker.Unsubscribe(Handle); + messageBroker.Unsubscribe(Handle); + } + + private void Handle(MessagePayload obj) + { + var payload = obj.What; + + if (objectManager.AddNewObject(payload.EquipmentRoster, out string EquipmentRosterId) == false) return; + + var message = new NetworkCreateEquipmentRoster(EquipmentRosterId); + network.SendAll(message); + } + + private void Handle(MessagePayload obj) + { + var payload = obj.What; + + var EquipmentRoster = ObjectHelper.SkipConstructor(); + if (objectManager.AddExisting(payload.EquipmentRosterId, EquipmentRoster) == false) + { + Logger.Error("Failed to add existing EquipmentRoster, {id}", payload.EquipmentRosterId); + return; + } + } + } +} \ No newline at end of file diff --git a/source/GameInterface/Services/EquipmentRoster/Messages/EquipmentRosterCreated.cs b/source/GameInterface/Services/EquipmentRoster/Messages/EquipmentRosterCreated.cs new file mode 100644 index 000000000..00b1f77e5 --- /dev/null +++ b/source/GameInterface/Services/EquipmentRoster/Messages/EquipmentRosterCreated.cs @@ -0,0 +1,15 @@ +using Common.Messaging; +using TaleWorlds.Core; + +namespace GameInterface.Services.EquipmentRoster.Messages +{ + internal class EquipmentRosterCreated : IEvent + { + public MBEquipmentRoster EquipmentRoster { get; } + + public EquipmentRosterCreated(MBEquipmentRoster equipmentRoster) + { + EquipmentRoster = equipmentRoster; + } + } +} diff --git a/source/GameInterface/Services/EquipmentRoster/Messages/NetworkCreateEquipmentRoster.cs b/source/GameInterface/Services/EquipmentRoster/Messages/NetworkCreateEquipmentRoster.cs new file mode 100644 index 000000000..34027d8aa --- /dev/null +++ b/source/GameInterface/Services/EquipmentRoster/Messages/NetworkCreateEquipmentRoster.cs @@ -0,0 +1,16 @@ +using Common.Messaging; +using ProtoBuf; + +namespace GameInterface.Services.EquipmentRoster.Messages +{ + [ProtoContract(SkipConstructor = true)] + internal class NetworkCreateEquipmentRoster : ICommand + { + [ProtoMember(1)] + public string EquipmentRosterId; + public NetworkCreateEquipmentRoster(string equipmentRosterId) + { + EquipmentRosterId = equipmentRosterId; + } + } +} diff --git a/source/GameInterface/Services/EquipmentRoster/Patches/EquipmentRosterLifetimePatches.cs b/source/GameInterface/Services/EquipmentRoster/Patches/EquipmentRosterLifetimePatches.cs new file mode 100644 index 000000000..c19efffa1 --- /dev/null +++ b/source/GameInterface/Services/EquipmentRoster/Patches/EquipmentRosterLifetimePatches.cs @@ -0,0 +1,41 @@ +using Common.Logging; +using Common.Messaging; +using GameInterface.Policies; +using GameInterface.Services.EquipmentRoster.Messages; +using HarmonyLib; +using Serilog; +using System; +using TaleWorlds.Core; + +namespace GameInterface.Services.EquipmentRoster.Patches +{ + /// + /// Lifetime Patches for EquipmentRoster + /// + [HarmonyPatch] + internal class EquipmentRosterLifetimePatches + { + private static ILogger Logger = LogManager.GetLogger(); + + [HarmonyPatch(typeof(MBEquipmentRoster), MethodType.Constructor)] + [HarmonyPrefix] + private static bool CreateEquipmentRosterPrefix(ref MBEquipmentRoster __instance) + { + // Call original if we call this function + if (CallOriginalPolicy.IsOriginalAllowed()) return true; + + if (ModInformation.IsClient) + { + Logger.Error("Client created unmanaged {name}\n" + + "Callstack: {callstack}", typeof(MBEquipmentRoster), Environment.StackTrace); + return false; + } + + var message = new EquipmentRosterCreated(__instance); + + MessageBroker.Instance.Publish(__instance, message); + + return true; + } + } +}