using Ananta.Server.ClientData.Client4229938; using Ananta.Server.Configuration; using Auto = Ananta.Server.RpcTypes.Client4229938.Auto; using SceneMethods = Ananta.Server.RpcTypes.Client4229938.Methods.GameScene; namespace Ananta.Server.Protocol.Client4229938; /// /// Typed factories for combat RPCs. Weapon/style/skill values come from the extracted build-4229938 /// client tables; private-server.json only supplies private-server balance and identity values. /// internal static class CombatCodec { private static CombatSettings Settings => PrivateServerConfigStore.Current.Gameplay.Combat; internal static float MaxHp => Settings.MaxHp; internal static uint UltimateResourceId => Settings.Resource.Id; internal static float UltimateResourceMax => CombatCatalogRepository.ResourceMaximum(UltimateResourceId, Settings.Resource.Max); internal static IReadOnlyDictionary AllResourceMaximums => CombatCatalogRepository.ResourceMaximums; internal static CombatLoadout Loadout(uint templateId) => CombatCatalogRepository.Loadout(templateId); internal static CombatWeaponDefinition DefaultWeapon(uint templateId) => Loadout(templateId).DefaultWeapon; internal static CombatWeaponDefinition? Weapon(uint templateId, ulong instanceId) => CombatCatalogRepository.Weapon(templateId, instanceId); internal static CombatWeaponDefinition? WeaponAt(uint templateId, int index) => Loadout(templateId).WeaponAt(index); internal static SceneMethods.PlayerFightStyleUnlockChangeInfo FightStyleUnlock() { var values = CombatCatalogRepository.UnlockedStyleIds.ToDictionary(x => x, _ => true); return new SceneMethods.PlayerFightStyleUnlockChangeInfo { playerInfoFightStyle = new SceneMethods.PlayerInfoFightStyle { fightStyles = values.ToDictionary(x => x.Key, x => x.Value) }, addOrUpdateUnlockInfo = values }; } internal static SceneMethods.SpiritFightTypeChangeAction FightStyleAction(uint templateId) => FightStyleAction(templateId, Array.Empty>()); internal static SceneMethods.SpiritFightTypeChangeAction FightStyleAction( uint templateId, IEnumerable> overrides) { var styleByType = Loadout(templateId).StyleByType.ToDictionary(x => x.Key, x => x.Value); foreach (var (typeId, styleId) in overrides) styleByType[typeId] = styleId; return new SceneMethods.SpiritFightTypeChangeAction { templateId = templateId, fullInfo = new SceneMethods.SpiritFightStyleInfo { fightStyleInfo = styleByType.ToDictionary(x => x.Key, x => x.Value) }, addOrUpdateInfo = styleByType }; } internal static SceneMethods.SyncChangeSkill CommonBinding(ulong unitId, CombatStyleDefinition style) => WorldCodec.SkillBinding(unitId, style.CommonSkill); internal static SceneMethods.SyncChangeSkill HeavyAttackBinding(ulong unitId, CombatStyleDefinition style) => WorldCodec.SkillBinding(unitId, style.HeavyCommonSkill); internal static SceneMethods.SyncChangeSkill DodgeBinding(ulong unitId, CombatStyleDefinition style) => WorldCodec.SkillBinding(unitId, style.DodgeSkill); internal static SceneMethods.SyncChangeSkill ControlBinding(ulong unitId, CombatStyleDefinition style) => WorldCodec.SkillBinding(unitId, style.ControlSkill); internal static SceneMethods.SyncChangeSkill ActiveBinding( ulong unitId, CombatWeaponDefinition weapon, CombatStyleDefinition style) => WorldCodec.SkillBinding(unitId, weapon.ActiveSkill(style)); internal static SceneMethods.SyncChangeSkill UniqueBinding( ulong unitId, CombatWeaponDefinition weapon, CombatStyleDefinition style) => WorldCodec.SkillBinding(unitId, weapon.UniqueSkill(style)); internal static SceneMethods.SyncPlayerAllSkillChargeData AllSkillCharges( ulong unitId, CombatWeaponDefinition weapon, CombatStyleDefinition style) { var body = new SceneMethods.SyncPlayerAllSkillChargeData { unitId = unitId }; void Full(uint skillId) { if (skillId == 0 || body.charges.ContainsKey(skillId)) return; var period = weapon.Cooldown(style, skillId, 0f); var maximum = weapon.MaximumCharges(style, skillId); // A zero Cooldown means the skill is input/animation driven, not charge-count driven. // Publishing ChargeData for such skills makes the client consume an invented stock and // interferes with click/hold/release transitions in SkillJumpConfig. if (period <= 0f || maximum == 0) return; body.charges[skillId] = new SceneMethods.ChargeData { current = maximum, readyRatio = 1f, period = period, max = maximum, timestamp = 0d }; } foreach (var skillId in weapon.EffectiveSkillIds(style)) Full(skillId); Full(weapon.ActiveSkill(style)); Full(weapon.UniqueSkill(style)); return body; } internal static SceneMethods.SyncAttachBattleModule AttachBattleModule(ulong unitId) => new() { unitId = unitId }; internal static SceneMethods.SyncFightResource FightResource(ulong unitId, uint resourceId, float value) => new() { unitId = unitId, resourceId = resourceId, value = value }; internal static SceneMethods.SyncFightResourceFreeState FightResourceFreeState(ulong unitId, uint resourceId, bool isFree) => new() { unitId = unitId, resourceId = resourceId, isFree = isFree }; internal static SceneMethods.SyncUnitHp UnitHp(ulong unitId, float hp) => new() { unitId = unitId, hp = hp }; internal static SceneMethods.SyncSpiritUnitUrbanAttrs UrbanAttrs(ulong unitId) => new() { unitId = unitId, abilities = Settings.DefaultUrbanAbilities.ToList() }; internal static SceneMethods.SyncUnitAttrs UnitAttrs(ulong unitId, float maxHp) => new() { unitId = unitId, attrs = new Dictionary { [1] = maxHp, [2] = Settings.Attack, [7] = Settings.Defense, [10] = Settings.MoveSpeedMultiplier } }; internal static SceneMethods.SyncSpiritLastUsedWeapon SpiritLastUsedWeapon(uint templateId, ulong weaponInstanceId) => new() { templateId = templateId, weaponInstanceId = weaponInstanceId }; internal static SceneMethods.SpiritSwitchWeaponAction SpiritSwitchWeapon(ulong unitId, ulong weaponInstanceId) => new() { spiritUid = unitId, weaponInstanceId = weaponInstanceId, reason = 0 }; internal static IReadOnlyList ArmoryWeapons() => CombatCatalogRepository.AccountWeapons.Select(RuntimePayloadFactory.WeaponData).ToArray(); internal static SceneMethods.SpiritWeaponDetail SpiritWeaponSnapshot( ulong unitId, uint templateId, ulong currentWeaponInstanceId, IReadOnlyList? slotWeapons = null) { var loadout = Loadout(templateId); slotWeapons ??= loadout.Slots; if (!slotWeapons.Any(x => x?.InstanceId == currentWeaponInstanceId)) currentWeaponInstanceId = slotWeapons.FirstOrDefault(x => x is not null)?.InstanceId ?? loadout.DefaultWeapon.InstanceId; return new SceneMethods.SpiritWeaponDetail { SpiritTid = templateId, SpiritUid = unitId, CurrentWeaponUid = currentWeaponInstanceId, WeaponSlots = slotWeapons.Select(weapon => weapon is null ? null : WeaponDetail(weapon)).ToList(), CurrentTempWeapon = null, TempWeaponSlots = null, VirtualWeaponSlots = [] }; } /// /// WeaponManager.lua reads OperatorFlags as four independent bytes — GetFlag(flags, n) is /// (flags >> (n * 8)) & 0xFF, not a bit test. Byte 1 is the discard gate: every consumer in the /// client reads GetFlag(OperatorFlags, 1) ~= 1 as CantDiscard / isNotDiscard, so a weapon /// is droppable, and the long-press-Q psychokinesis skill is enabled, only when byte 1 is 1. /// /// Shipping 0 marked every weapon non-discardable. That is why no capture ever contained an /// AskDiscardWeapon (BackCircleWeaponStore returns before sending one when CantDiscard is true) /// and why long-press-Q wedged the unit in its absorb animation and slid backwards. /// Byte 2 is the task-weapon marker (IsTask) and deliberately stays 0. /// internal const uint WeaponOperatorFlagsDiscardable = 1u << 8; internal static SceneMethods.WeaponDetail WeaponDetail(CombatWeaponDefinition weapon) => new() { SourceAgentSpoonId = 0, SourceAgentId = 0, SourceSceneItemId = 0, IsLocked = false, SourceSceneItemTaskUId = 0, TemplateId = weapon.TemplateId, Durability = weapon.InitialClientDurability, InstanceId = weapon.InstanceId, EventId = 0, ReceivedTimeStamp = 0d, OperatorFlags = WeaponOperatorFlagsDiscardable, SpecialLabel = null, SceneItemHp = 1f, StackCount = 1, FightStyleId = weapon.Style.Id, MagazineAmmo = weapon.InitialMagazineAmmo, BulletDatas = new Auto.WeaponBulletDatas { BulletId = weapon.BulletId }, BindPid = PrivateServerConfigStore.Current.Player.Pid, IsPlayerLocked = false }; /// DiscardWeaponReason.Remove — disarm only; unlike Throw/RemoveAndDrop it carries no /// "and drop it into the world" meaning, so the client will not mint a new ground drop. internal const byte DiscardWeaponReasonRemove = 5; internal static SceneMethods.SpiritRemoveWeaponAction SpiritRemoveWeapon( uint spiritTemplateId, ulong spiritUnitId, ulong weaponInstanceId) => new() { SpiritTid = spiritTemplateId, SpiritUid = spiritUnitId, WeaponUid = weaponInstanceId, Reason = DiscardWeaponReasonRemove, }; internal static SceneMethods.SpiritAddWeaponAction SpiritAddWeapon( uint spiritTemplateId, ulong spiritUnitId, int slotIndex, CombatWeaponDefinition weapon) => new() { SpiritTid = spiritTemplateId, SpiritUid = spiritUnitId, SlotIndex = slotIndex, Weapon = WeaponDetail(weapon), }; internal static SceneMethods.SpiritWeaponDurabilityChangedAction WeaponDurabilityChanged( uint spiritTemplateId, ulong spiritUnitId, CombatWeaponDefinition weapon, int durability, int magazineAmmo, uint bulletId) => new() { SpiritTid = spiritTemplateId, SpiritUid = spiritUnitId, WeaponInstanceId = weapon.InstanceId, Durability = durability, MagazineAmmo = magazineAmmo, CurrentBulletId = bulletId, StackCount = 1, }; internal static bool IsKnownSkill( uint templateId, ulong weaponInstanceId, uint skillId) { var weapon = Weapon(templateId, weaponInstanceId) ?? DefaultWeapon(templateId); return weapon.AllowsSkill(weapon.Style, skillId); } internal static SceneMethods.SyncBreakSkill BreakSkill(ulong unitId) => new() { unitId = unitId }; }