private static bool SharedShouldFireMore(WeaponState state) { if (!state.IsFiring) { return(false); } // is firing delay completed? var canStopFiring = state.DamageApplyDelaySecondsRemains <= 0; if (canStopFiring && IsServer && state.ServerLastClientReportedShotsDoneCount.HasValue) { // cannot stop firing if not all the ammo are fired yet if (state.ShotsDone < state.ServerLastClientReportedShotsDoneCount) { // let's spend all the remaining ammo before stopping firing canStopFiring = false; //Logger.Dev("Not all shots done yet, delay stopping firing: shotsDone=" // + state.ShotsDone // + " requiresShotsDone=" // + state.ServerLastClientReportedShotsDoneCount); } } return(!canStopFiring); }
private static void SharedCallOnWeaponFinished(WeaponState state, ICharacter character) { if (IsServer) { ServerCheckFiredShotsMismatch(state, character); } state.IsEventWeaponStartSent = false; if (IsClient) { // finished firing weapon on Client-side WeaponSystemClientDisplay.OnWeaponFinished(character); } else // if this is Server { // notify other clients about finished firing weapon using (var scopedBy = Api.Shared.GetTempList <ICharacter>()) { Server.World.GetScopedByPlayers(character, scopedBy); Instance.CallClient( scopedBy, _ => _.ClientRemote_OnWeaponFinished(character)); } } }
public static void SharedUpdateReloading(WeaponState weaponState, ICharacter character, ref double deltaTime) { var reloadingState = weaponState.WeaponReloadingState; if (reloadingState == null) { return; } // process reloading reloadingState.SecondsToReloadRemains -= deltaTime; if (reloadingState.SecondsToReloadRemains > 0) { // need more time to reload deltaTime = 0; return; } // reloaded deltaTime = -reloadingState.SecondsToReloadRemains; reloadingState.SecondsToReloadRemains = 0; SharedProcessWeaponReload(character, weaponState); weaponState.ShotsDone = 0; weaponState.ServerLastClientReportedShotsDoneCount = null; }
public static void RebuildWeaponCache( ICharacter character, WeaponState weaponState) { DamageDescription damageDescription = null; var item = weaponState.ActiveItemWeapon; var protoItem = weaponState.ActiveProtoWeapon; if (protoItem == null) { return; } if (protoItem.OverrideDamageDescription != null) { damageDescription = protoItem.OverrideDamageDescription; } else if (item != null) { var weaponPrivateState = item.GetPrivateState <WeaponPrivateState>(); damageDescription = weaponPrivateState.CurrentProtoItemAmmo?.DamageDescription; } weaponState.WeaponCache = new WeaponFinalCache( character, character.SharedGetFinalStatsCache(), item, weaponState.ActiveProtoWeapon, damageDescription); }
private static void ServerCheckFiredShotsMismatch(WeaponState state, ICharacter character) { var ammoConsumptionPerShot = state.ProtoWeapon.AmmoConsumptionPerShot; if (ammoConsumptionPerShot == 0) { // weapon doesn't use any ammo - no problem with possible desync return; } if (!WeaponAmmoSystem.IsResetsShotsDoneNumberOnReload(state.ProtoWeapon)) { // this weapon can keep firing after the reload on the server side return; } var requestedShotsCount = state.ServerLastClientReportedShotsDoneCount; if (!requestedShotsCount.HasValue) { return; } var extraShotsDone = (int)(state.ShotsDone - (long)requestedShotsCount.Value); state.ServerLastClientReportedShotsDoneCount = null; if (extraShotsDone == 0) { return; } if (extraShotsDone < 0) { // should never happen as server should fire as much as client requested, always return; } var itemWeapon = state.ItemWeapon; if (itemWeapon == null) { return; } Logger.Important($"Shots count mismatch: requested={requestedShotsCount} actualShotsDone={state.ShotsDone}", character); Instance.CallClient(character, _ => _.ClientRemote_FixAmmoCount(itemWeapon, extraShotsDone)); }
private static void SharedCallOnWeaponStart(WeaponState state, ICharacter character) { Api.Assert(!state.IsEventWeaponStartSent, "Firing event must be not set"); state.IsEventWeaponStartSent = true; if (IsClient) { // start firing weapon on Client-side WeaponSystemClientDisplay.OnWeaponStart(character); } else // if IsServer { using var scopedBy = Api.Shared.GetTempList <ICharacter>(); Server.World.GetScopedByPlayers(character, scopedBy); Instance.CallClient(scopedBy, _ => _.ClientRemote_OnWeaponStart(character)); } }
private static void ServerCheckFiredShotsMismatch(WeaponState state, ICharacter character) { var ammoConsumptionPerShot = state.ActiveProtoWeapon.AmmoConsumptionPerShot; if (ammoConsumptionPerShot == 0) { // weapon doesn't use any ammo - no problem with possible desync return; } var requestedShotsCount = state.ServerLastClientReportedShotsDoneCount; if (!requestedShotsCount.HasValue) { return; } var extraShotsDone = (int)(state.ShotsDone - (long)requestedShotsCount.Value); state.ServerLastClientReportedShotsDoneCount = null; if (extraShotsDone == 0) { return; } if (extraShotsDone < 0) { // should never happen as server should fire as much as client requested, always return; } var itemWeapon = state.ActiveItemWeapon; if (itemWeapon == null) { return; } //Logger.Dev($"Shots count mismatch: requested={requestedShotsCount} actualShotsDone={state.ShotsDone}"); Instance.CallClient(character, _ => _.ClientRemote_FixAmmoCount(itemWeapon, extraShotsDone)); }
public static void SharedRebuildWeaponCache( ICharacter character, WeaponState weaponState) { DamageDescription damageDescription = null; var item = weaponState.ItemWeapon; var protoItem = weaponState.ProtoWeapon; if (protoItem == null) { return; } IProtoItemAmmo protoAmmo = null; if (item != null) { var weaponPrivateState = item.GetPrivateState <WeaponPrivateState>(); protoAmmo = weaponPrivateState.CurrentProtoItemAmmo; } if (protoItem.OverrideDamageDescription != null) { damageDescription = protoItem.OverrideDamageDescription; } else if (protoAmmo is IAmmoWithCustomWeaponCacheDamageDescription customAmmo) { damageDescription = customAmmo.DamageDescriptionForWeaponCache; } else if (protoAmmo != null) { damageDescription = protoAmmo.DamageDescription; } weaponState.WeaponCache = new WeaponFinalCache( character, character.SharedGetFinalStatsCache(), item, weaponState.ProtoWeapon, protoAmmo, damageDescription); }
private static void SharedCallOnWeaponInputStop(WeaponState state, ICharacter character) { Api.Assert(state.IsEventWeaponStartSent, "Firing event must be set"); state.IsEventWeaponStartSent = false; if (IsClient) { // finished firing weapon on Client-side WeaponSystemClientDisplay.OnWeaponInputStop(character); } else // if this is Server { // notify other clients about finished firing weapon using var scopedBy = Api.Shared.GetTempList <ICharacter>(); Server.World.GetScopedByPlayers(character, scopedBy); Instance.CallClient( scopedBy, _ => _.ClientRemote_OnWeaponInputStop(character)); } }
// send notification about reloading to players in scope (so they can play a sound) private static void ServerNotifyAboutReloading(ICharacter character, WeaponState weaponState, bool isFinished) { using var scopedBy = Api.Shared.GetTempList <ICharacter>(); Server.World.GetScopedByPlayers(character, scopedBy); scopedBy.Remove(character); if (scopedBy.Count == 0) { return; } if (isFinished) { Instance.CallClient(scopedBy.AsList(), _ => _.ClientRemote_OnOtherCharacterReloaded(character, weaponState.ProtoWeapon)); } else { Instance.CallClient(scopedBy.AsList(), _ => _.ClientRemote_OnOtherCharacterReloading(character, weaponState.ProtoWeapon)); } }
public static void SharedUpdateReloading(WeaponState weaponState, ICharacter character, double deltaTime) { var reloadingState = weaponState.WeaponReloadingState; if (reloadingState is null) { return; } // process reloading reloadingState.SecondsToReloadRemains -= deltaTime; if (reloadingState.SecondsToReloadRemains > 0) { // need more time to reload return; } // reloaded reloadingState.SecondsToReloadRemains = 0; SharedProcessWeaponReload(character, weaponState, out var isAmmoTypeChanged); if (isAmmoTypeChanged || IsResetsShotsDoneNumberOnReload(weaponState.ProtoWeapon)) { weaponState.ShotsDone = 0; weaponState.ServerLastClientReportedShotsDoneCount = null; weaponState.CustomTargetPosition = null; //Api.Logger.Dev("Reset ServerLastClientReportedShotsDoneCount. Last value: " // + weaponState.ServerLastClientReportedShotsDoneCount); } weaponState.FirePatternCooldownSecondsRemains = 0; weaponState.IsIdleAutoReloadingAllowed = true; }
private static void SharedFireWeapon( ICharacter character, IItem weaponItem, IProtoItemWeapon protoWeapon, WeaponState weaponState) { if (!protoWeapon.SharedOnFire(character, weaponState)) { return; } var playerCharacterSkills = character.SharedGetSkills(); var protoWeaponSkill = playerCharacterSkills != null ? protoWeapon.WeaponSkillProto : null; if (IsServer) { protoWeaponSkill?.ServerOnShot(playerCharacterSkills); // give experience for shot CharacterUnstuckSystem.ServerTryCancelUnstuckRequest(character); } var weaponCache = weaponState.WeaponCache; if (weaponCache is null) { SharedRebuildWeaponCache(character, weaponState); weaponCache = weaponState.WeaponCache; } var characterCurrentVehicle = character.IsNpc ? null : character.SharedGetCurrentVehicle(); var isMeleeWeapon = protoWeapon is IProtoItemWeaponMelee; var characterProtoCharacter = (IProtoCharacterCore)character.ProtoCharacter; var fromPosition = characterProtoCharacter.SharedGetWeaponFireWorldPosition(character, isMeleeWeapon); var fireSpreadAngleOffsetDeg = protoWeapon.SharedUpdateAndGetFirePatternCurrentSpreadAngleDeg(weaponState); var collisionGroup = protoWeapon.CollisionGroup; using var allHitObjects = Shared.GetTempList <IWorldObject>(); var shotsPerFire = weaponCache.FireScatterPreset.ProjectileAngleOffets; foreach (var angleOffsetDeg in shotsPerFire) { SharedShotWeaponHitscan(character, protoWeapon, fromPosition, weaponCache, weaponState.CustomTargetPosition, characterProtoCharacter, fireSpreadAngleOffsetDeg + angleOffsetDeg, collisionGroup, isMeleeWeapon, characterCurrentVehicle, protoWeaponSkill, playerCharacterSkills, allHitObjects); } if (IsServer) { protoWeapon.ServerOnShot(character, weaponItem, protoWeapon, allHitObjects.AsList()); } }
/// <summary> /// Executed when a weapon must reload (after the reloading duration is completed). /// </summary> private static void SharedProcessWeaponReload(ICharacter character, WeaponState weaponState) { var weaponReloadingState = weaponState.WeaponReloadingState; // remove weapon reloading state weaponState.WeaponReloadingState = null; var itemWeapon = weaponReloadingState.Item; var itemWeaponProto = (IProtoItemWeapon)itemWeapon.ProtoGameObject; var itemWeaponPrivateState = itemWeapon.GetPrivateState <WeaponPrivateState>(); var weaponAmmoCount = (int)itemWeaponPrivateState.AmmoCount; var weaponAmmoCapacity = itemWeaponProto.AmmoCapacity; var selectedProtoItemAmmo = weaponReloadingState.ProtoItemAmmo; if (weaponAmmoCount > 0) { if (selectedProtoItemAmmo != itemWeaponPrivateState.CurrentProtoItemAmmo && weaponAmmoCount > 0) { // unload current ammo if (IsServer) { Server.Items.CreateItem( toCharacter: character, protoItem: itemWeaponPrivateState.CurrentProtoItemAmmo, count: (ushort)weaponAmmoCount); } Logger.Info( $"Weapon ammo unloaded: {itemWeapon} -> {weaponAmmoCount} {itemWeaponPrivateState.CurrentProtoItemAmmo})", character); weaponAmmoCount = 0; itemWeaponPrivateState.AmmoCount = 0; } else // if the same ammo type is loaded if (weaponAmmoCount == weaponAmmoCapacity) { // already completely loaded Logger.Info( $"Weapon reloading cancelled: {itemWeapon} - no reloading is required ({weaponAmmoCount}/{weaponAmmoCapacity} {selectedProtoItemAmmo})", character); return; } } else // if ammoCount == 0 if (selectedProtoItemAmmo == null && itemWeaponPrivateState.CurrentProtoItemAmmo == null) { Logger.Info( $"Weapon reloading cancelled: {itemWeapon} - already unloaded ({weaponAmmoCount}/{weaponAmmoCapacity})", character); return; } if (selectedProtoItemAmmo != null) { var selectedAmmoGroup = SharedGetCompatibleAmmoGroups(character, itemWeaponProto) .FirstOrDefault(g => g.Key == selectedProtoItemAmmo); if (selectedAmmoGroup == null) { Logger.Warning( $"Weapon reloading impossible: {itemWeapon} - no ammo of the required type ({selectedProtoItemAmmo})", character); return; } var ammoItems = SharedSelectAmmoItemsFromGroup(selectedAmmoGroup, ammoCountNeed: weaponAmmoCapacity - weaponAmmoCount); foreach (var request in ammoItems) { var itemAmmo = request.Item; if (itemAmmo.ProtoItem != selectedProtoItemAmmo) { Logger.Error( "Trying to load multiple ammo types in one reloading: " + ammoItems.Select(a => a.Item.ProtoItem).GetJoinedString(), character); break; } int ammoToSubstract; var itemAmmoCount = itemAmmo.Count; if (itemAmmoCount == 0) { continue; } if (request.Count != itemAmmoCount) { if (request.Count < itemAmmoCount) { itemAmmoCount = request.Count; } else if (IsServer) { Logger.Warning( $"Trying to take more ammo to reload than player have: {itemAmmo} requested {request.Count}. Will reload as much as possible only.", character); } } if (weaponAmmoCount + itemAmmoCount >= weaponAmmoCapacity) { // there are more than enough ammo in that item stack to fully refill the weapon ammoToSubstract = weaponAmmoCapacity - weaponAmmoCount; weaponAmmoCount = weaponAmmoCapacity; } else { // consume full item stack ammoToSubstract = itemAmmoCount; weaponAmmoCount += itemAmmoCount; } // check if character owns this item if (itemAmmo.Container.OwnerAsCharacter != character) { Logger.Error("The character doesn't own " + itemAmmo + " - cannot use it to reload", character); continue; } // reduce ammo item count if (IsServer) { Server.Items.SetCount( itemAmmo, itemAmmo.Count - ammoToSubstract, byCharacter: character); } if (weaponAmmoCount == weaponAmmoCapacity) { // the weapon is fully reloaded, no need to subtract ammo from the next ammo items break; } } } if (itemWeaponPrivateState.CurrentProtoItemAmmo != selectedProtoItemAmmo) { // another ammo type selected itemWeaponPrivateState.CurrentProtoItemAmmo = selectedProtoItemAmmo; // reset weapon cache (it will be re-calculated on next fire processing) weaponState.WeaponCache = null; } if (weaponAmmoCount < 0 || weaponAmmoCount > weaponAmmoCapacity) { Logger.Error( "Something is completely wrong during reloading! Result ammo count is: " + weaponAmmoCount); weaponAmmoCount = 0; } itemWeaponPrivateState.AmmoCount = (ushort)weaponAmmoCount; Logger.Info( $"Weapon reloaded: {itemWeapon} - ammo {weaponAmmoCount}/{weaponAmmoCapacity} {(selectedProtoItemAmmo?.ToString() ?? "<no ammo>")}", character); }
private static void SharedFireWeapon( ICharacter character, IItem weaponItem, IProtoItemWeapon protoWeapon, WeaponState weaponState) { protoWeapon.SharedOnFire(character, weaponState); var playerCharacterSkills = character.SharedGetSkills(); var protoWeaponSkill = playerCharacterSkills != null ? protoWeapon.WeaponSkillProto : null; if (IsServer) { // give experience for shot protoWeaponSkill?.ServerOnShot(playerCharacterSkills); } var weaponCache = weaponState.WeaponCache; if (weaponCache == null) { // calculate new weapon cache RebuildWeaponCache(character, weaponState); weaponCache = weaponState.WeaponCache; } // raycast possible victims var fromPosition = character.Position + (0, character.ProtoCharacter.CharacterWorldWeaponOffset); var toPosition = fromPosition + new Vector2D(weaponCache.RangeMax, 0) .RotateRad(character.ProtoCharacter.SharedGetRotationAngleRad(character)); var collisionGroup = protoWeapon is IProtoItemWeaponMelee ? CollisionGroups.HitboxMelee : CollisionGroups.HitboxRanged; using (var lineTestResults = character.PhysicsBody.PhysicsSpace.TestLine( fromPosition: fromPosition, toPosition: toPosition, collisionGroup: collisionGroup)) { var damageMultiplier = 1d; var isMeleeWeapon = protoWeapon is IProtoItemWeaponMelee; var hitObjects = new List <WeaponHitData>(isMeleeWeapon ? 1 : lineTestResults.Count); foreach (var testResult in lineTestResults) { var testResultPhysicsBody = testResult.PhysicsBody; var attackedProtoTile = testResultPhysicsBody.AssociatedProtoTile; if (attackedProtoTile != null) { if (attackedProtoTile.Kind != TileKind.Solid) { // non-solid obstacle - skip continue; } // tile on the way - blocking damage ray break; } var damagedObject = testResultPhysicsBody.AssociatedWorldObject; if (damagedObject == character) { // ignore collision with self continue; } if (!(damagedObject.ProtoGameObject is IDamageableProtoWorldObject damageableProto)) { // shoot through this object continue; } if (!damageableProto.SharedOnDamage( weaponCache, damagedObject, damageMultiplier, out var obstacleBlockDamageCoef, out var damageApplied)) { // not hit continue; } if (IsServer) { weaponCache.ProtoWeapon .ServerOnDamageApplied(weaponCache.Weapon, character, damagedObject, damageApplied); if (damageApplied > 0 && protoWeaponSkill != null) { // give experience for damage protoWeaponSkill.ServerOnDamageApplied(playerCharacterSkills, damagedObject, damageApplied); if (damagedObject is ICharacter damagedCharacter && damagedCharacter.GetPublicState <ICharacterPublicState>().CurrentStats.HealthCurrent <= 0) { // give weapon experience for kill Logger.Info("Killed " + damagedCharacter, character); protoWeaponSkill.ServerOnKill(playerCharacterSkills, killedCharacter: damagedCharacter); if (damagedCharacter.ProtoCharacter is ProtoCharacterMob protoMob) { // give hunting skill experience for mob kill var experience = SkillHunting.ExperienceForKill; experience *= protoMob.MobKillExperienceMultiplier; if (experience > 0) { playerCharacterSkills.ServerAddSkillExperience <SkillHunting>(experience); } } } } } if (obstacleBlockDamageCoef < 0 || obstacleBlockDamageCoef > 1) { Logger.Error( "Obstacle block damage coefficient should be >= 0 and <= 1 - wrong calculation by " + damageableProto); break; } //var hitPosition = testResultPhysicsBody.Position + testResult.Penetration; hitObjects.Add(new WeaponHitData(damagedObject)); //, hitPosition)); if (isMeleeWeapon) { // currently melee weapon could attack only one object on the ray break; } damageMultiplier = damageMultiplier * (1.0 - obstacleBlockDamageCoef); if (damageMultiplier <= 0) { // target blocked the damage ray break; } } if (hitObjects.Count > 0) { if (IsClient) { // display weapon shot on Client-side WeaponSystemClientDisplay.OnWeaponHit(protoWeapon, hitObjects); } else // if server { // display damages on clients in scope of every damaged object using (var scopedBy = Api.Shared.GetTempList <ICharacter>()) { foreach (var hitObject in hitObjects) { if (hitObject.WorldObject.IsDestroyed) { continue; } Server.World.GetScopedByPlayers(hitObject.WorldObject, scopedBy); scopedBy.Remove(character); if (scopedBy.Count == 0) { continue; } Instance.CallClient(scopedBy, _ => _.ClientRemote_OnWeaponHit(protoWeapon, hitObject)); scopedBy.Clear(); } } } } if (IsServer) { protoWeapon.ServerOnShot(character, weaponItem, protoWeapon, hitObjects); } } }
/// <summary> /// Executed when a weapon must reload (after the reloading duration is completed). /// </summary> private static void SharedProcessWeaponReload(ICharacter character, WeaponState weaponState) { var weaponReloadingState = weaponState.WeaponReloadingState; // remove weapon reloading state weaponState.WeaponReloadingState = null; var itemWeapon = weaponReloadingState.Item; var itemWeaponProto = (IProtoItemWeapon)itemWeapon.ProtoGameObject; var itemWeaponPrivateState = itemWeapon.GetPrivateState <WeaponPrivateState>(); var weaponAmmoCount = (int)itemWeaponPrivateState.AmmoCount; var weaponAmmoCapacity = itemWeaponProto.AmmoCapacity; var selectedProtoItemAmmo = weaponReloadingState.ProtoItemAmmo; var currentProtoItemAmmo = itemWeaponPrivateState.CurrentProtoItemAmmo; if (weaponAmmoCount > 0) { if (selectedProtoItemAmmo != currentProtoItemAmmo && weaponAmmoCount > 0) { // unload current ammo if (IsServer) { var result = Server.Items.CreateItem( toCharacter: character, protoItem: currentProtoItemAmmo, count: (ushort)weaponAmmoCount); if (!result.IsEverythingCreated) { // cannot unload current ammo - no space, try to unload to the ground result.Rollback(); var tile = Api.Server.World.GetTile(character.TilePosition); var groundContainer = ObjectGroundItemsContainer .ServerTryGetOrCreateGroundContainerAtTileOrNeighbors(tile); if (groundContainer == null) { // cannot unload current ammo to the ground - no free space around character Instance.CallClient(character, _ => _.ClientRemote_NoSpaceForUnloadedAmmo(currentProtoItemAmmo)); return; } result = Server.Items.CreateItem( container: groundContainer, protoItem: currentProtoItemAmmo, count: (ushort)weaponAmmoCount); if (!result.IsEverythingCreated) { // cannot unload current ammo to the ground - no space in ground containers near the character result.Rollback(); Instance.CallClient(character, _ => _.ClientRemote_NoSpaceForUnloadedAmmo( currentProtoItemAmmo)); return; } // notify player that there were not enough space in inventory so the items were dropped to the ground NotificationSystem.ServerSendNotificationNoSpaceInInventoryItemsDroppedToGround( character, result.ItemAmounts.First().Key?.ProtoItem); } } Logger.Info( $"Weapon ammo unloaded: {itemWeapon} -> {weaponAmmoCount} {currentProtoItemAmmo})", character); weaponAmmoCount = 0; itemWeaponPrivateState.AmmoCount = 0; } else // if the same ammo type is loaded if (weaponAmmoCount == weaponAmmoCapacity) { // already completely loaded Logger.Info( $"Weapon reloading cancelled: {itemWeapon} - no reloading is required ({weaponAmmoCount}/{weaponAmmoCapacity} {selectedProtoItemAmmo})", character); return; } } else // if ammoCount == 0 if (selectedProtoItemAmmo == null && currentProtoItemAmmo == null) { Logger.Info( $"Weapon reloading cancelled: {itemWeapon} - already unloaded ({weaponAmmoCount}/{weaponAmmoCapacity})", character); return; } if (selectedProtoItemAmmo != null) { var selectedAmmoGroup = SharedGetCompatibleAmmoGroups(character, itemWeaponProto) .FirstOrDefault(g => g.Key == selectedProtoItemAmmo); if (selectedAmmoGroup == null) { Logger.Warning( $"Weapon reloading impossible: {itemWeapon} - no ammo of the required type ({selectedProtoItemAmmo})", character); return; } var ammoItems = SharedSelectAmmoItemsFromGroup(selectedAmmoGroup, ammoCountNeed: weaponAmmoCapacity - weaponAmmoCount); foreach (var request in ammoItems) { var itemAmmo = request.Item; if (itemAmmo.ProtoItem != selectedProtoItemAmmo) { Logger.Error( "Trying to load multiple ammo types in one reloading: " + ammoItems.Select(a => a.Item.ProtoItem).GetJoinedString(), character); break; } int ammoToSubstract; var itemAmmoCount = itemAmmo.Count; if (itemAmmoCount == 0) { continue; } if (request.Count != itemAmmoCount) { if (request.Count < itemAmmoCount) { itemAmmoCount = request.Count; } else if (IsServer) { Logger.Warning( $"Trying to take more ammo to reload than player have: {itemAmmo} requested {request.Count}. Will reload as much as possible only.", character); } } if (weaponAmmoCount + itemAmmoCount >= weaponAmmoCapacity) { // there are more than enough ammo in that item stack to fully refill the weapon ammoToSubstract = weaponAmmoCapacity - weaponAmmoCount; weaponAmmoCount = weaponAmmoCapacity; } else { // consume full item stack ammoToSubstract = itemAmmoCount; weaponAmmoCount += itemAmmoCount; } // check if character owns this item if (itemAmmo.Container.OwnerAsCharacter != character) { Logger.Error("The character doesn't own " + itemAmmo + " - cannot use it to reload", character); continue; } // reduce ammo item count if (IsServer) { Server.Items.SetCount( itemAmmo, itemAmmo.Count - ammoToSubstract, byCharacter: character); } if (weaponAmmoCount == weaponAmmoCapacity) { // the weapon is fully reloaded, no need to subtract ammo from the next ammo items break; } } } if (currentProtoItemAmmo != selectedProtoItemAmmo) { // another ammo type selected itemWeaponPrivateState.CurrentProtoItemAmmo = selectedProtoItemAmmo; // reset weapon cache (it will be re-calculated on next fire processing) weaponState.WeaponCache = null; } if (weaponAmmoCount < 0 || weaponAmmoCount > weaponAmmoCapacity) { Logger.Error( "Something is completely wrong during reloading! Result ammo count is: " + weaponAmmoCount); weaponAmmoCount = 0; } itemWeaponPrivateState.AmmoCount = (ushort)weaponAmmoCount; Logger.Info( $"Weapon reloaded: {itemWeapon} - ammo {weaponAmmoCount}/{weaponAmmoCapacity} {selectedProtoItemAmmo?.ToString() ?? "<no ammo>"}", character); }
public static void SharedUpdateCurrentWeapon( ICharacter character, WeaponState state, double deltaTime) { var protoWeapon = state.ActiveProtoWeapon; if (protoWeapon == null) { return; } if (state.CooldownSecondsRemains > 0) { // decrease cooldown state.CooldownSecondsRemains -= deltaTime; } if (!state.IsFiring) { WeaponAmmoSystem.SharedUpdateReloading(state, character, ref deltaTime); } if (deltaTime <= 0) { // the weapon reloading process is consumed the whole delta time return; } if (state.SharedGetInputIsFiring() && !character.IsOnline) { state.SetInputIsFiring(false); } if (state.SharedGetInputIsFiring() && StatusEffectDazed.SharedIsCharacterDazed(character, StatusEffectDazed.NotificationCannotAttackWhileDazed)) { state.SetInputIsFiring(false); } // check ammo (if applicable to this weapon prototype) var canFire = protoWeapon.SharedCanFire(character, state); if (state.CooldownSecondsRemains > 0) { // firing cooldown is not completed if (!state.SharedGetInputIsFiring() && state.IsEventWeaponStartSent) { // not firing anymore SharedCallOnWeaponInputStop(state, character); } return; } var wasFiring = state.IsFiring; if (!state.IsFiring) { state.IsFiring = state.SharedGetInputIsFiring(); } else // if IsFiring { if (!SharedShouldFireMore(state)) { state.IsFiring = state.SharedGetInputIsFiring(); } } if (!canFire) { // cannot fire (no ammo, etc) state.IsFiring = false; } if (!state.IsFiring) { if (wasFiring) { // just stopped firing SharedCallOnWeaponFinished(state, character); } // the character is not firing // reset delay for the next shot (it will be set when firing starts next time) state.DamageApplyDelaySecondsRemains = 0; return; } // let's process what happens when we're in the firing mode if (!state.IsEventWeaponStartSent) { // started firing SharedCallOnWeaponStart(state, character); } if (state.DamageApplyDelaySecondsRemains <= 0) { // initialize delay to next shot state.DamageApplyDelaySecondsRemains = protoWeapon.DamageApplyDelay; SharedCallOnWeaponShot(character); } // decrease the remaining time to the damage application state.DamageApplyDelaySecondsRemains -= deltaTime; if (state.DamageApplyDelaySecondsRemains > 0) { // firing delay not completed return; } // firing delay completed state.ShotsDone++; //Logger.Dev("Weapon fired, shots done: " + state.ShotsDone); SharedFireWeapon(character, state.ActiveItemWeapon, protoWeapon, state); state.CooldownSecondsRemains += protoWeapon.FireInterval - protoWeapon.DamageApplyDelay; if (!protoWeapon.IsLoopedAttackAnimation) { // we don't want to stuck this animation in the last frame // that's fix for the issue: // "Fix extended animation "stuck" issue for mobs (like limbs stuck in the end position and movement animation appears broken)" state.IsEventWeaponStartSent = false; } }
public static void SharedUpdateCurrentWeapon( ICharacter character, WeaponState state, double deltaTime) { var protoWeapon = state.ProtoWeapon; if (protoWeapon == null) { return; } if (deltaTime > 0.4) { // too large delta time probably due to a frame skip deltaTime = 0.4; } if (state.CooldownSecondsRemains > 0) { state.CooldownSecondsRemains -= deltaTime; if (state.CooldownSecondsRemains < -0.2) { // clamp the remaining cooldown in case of a frame skip state.CooldownSecondsRemains = -0.2; } } if (state.ReadySecondsRemains > 0) { state.ReadySecondsRemains -= deltaTime; } if (state.FirePatternCooldownSecondsRemains > 0) { state.FirePatternCooldownSecondsRemains -= deltaTime; if (state.FirePatternCooldownSecondsRemains <= 0) { state.FirePatternCurrentShotNumber = 0; } } // TODO: restore this condition when we redo UI countdown animation for ViewModelHotbarItemWeaponOverlayControl.ReloadDurationSeconds //if (state.CooldownSecondsRemains <= 0) //{ WeaponAmmoSystem.SharedUpdateReloading(state, character, deltaTime); //} if (Api.IsServer && !character.ServerIsOnline && state.SharedGetInputIsFiring()) { state.SharedSetInputIsFiring(false); } // check ammo (if applicable to this weapon prototype) var canFire = (Api.IsClient || character.ServerIsOnline) && state.WeaponReloadingState is null && protoWeapon.SharedCanFire(character, state); if (state.CooldownSecondsRemains > 0) { // firing cooldown is not completed if (!state.SharedGetInputIsFiring() && state.IsEventWeaponStartSent) { // not firing anymore SharedCallOnWeaponInputStop(state, character); } return; } var wasFiring = state.IsFiring; if (!state.IsFiring) { state.IsFiring = state.SharedGetInputIsFiring(); } else // if IsFiring { if (!SharedShouldFireMore(state)) { state.IsFiring = state.SharedGetInputIsFiring(); } } if (!canFire) { // cannot fire (no ammo, etc) state.IsFiring = false; } if (!state.IsFiring) { if (wasFiring) { // just stopped firing SharedCallOnWeaponFinished(state, character); } // the character is not firing // reset delay for the next shot (it will be set when firing starts next time) state.DamageApplyDelaySecondsRemains = 0; return; } if (state.WeaponCache is null) { SharedRebuildWeaponCache(character, state); } // let's process what happens when we're in the firing mode if (!state.IsEventWeaponStartSent) { // started firing SharedCallOnWeaponStart(state, character); } if (state.DamageApplyDelaySecondsRemains <= 0) { // initialize delay to next shot state.DamageApplyDelaySecondsRemains = Shared.RoundDurationByServerFrameDuration(protoWeapon.DamageApplyDelay); SharedCallOnWeaponShot(character, protoWeapon); } // decrease the remaining time to the damage application state.DamageApplyDelaySecondsRemains -= deltaTime; if (state.DamageApplyDelaySecondsRemains > 0) { // firing delay not completed return; } // firing delay completed state.ShotsDone++; //Logger.Dev("Weapon fired, shots done: " + state.ShotsDone); SharedFireWeapon(character, state.ItemWeapon, protoWeapon, state); var cooldownDuration = Shared.RoundDurationByServerFrameDuration(protoWeapon.FireInterval) - Shared.RoundDurationByServerFrameDuration(protoWeapon.DamageApplyDelay); //Logger.Dev($"Cooldown adding: {cooldownDuration} for {protoWeapon}"); state.CooldownSecondsRemains += cooldownDuration; if (!protoWeapon.IsLoopedAttackAnimation) { // we don't want to stuck this animation in the last frame // that's fix for the issue: // "Fix extended animation "stuck" issue for mobs (like limbs stuck in the end position and movement animation appears broken)" state.IsEventWeaponStartSent = false; } }
/// <summary> /// Executed when a weapon must reload (after the reloading duration is completed). /// </summary> private static void SharedProcessWeaponReload( ICharacter character, WeaponState weaponState, out bool isAmmoTypeChanged) { var weaponReloadingState = weaponState.WeaponReloadingState; // remove weapon reloading state weaponState.WeaponReloadingState = null; var itemWeapon = weaponReloadingState.Item; var itemWeaponProto = (IProtoItemWeapon)itemWeapon.ProtoGameObject; var itemWeaponPrivateState = itemWeapon.GetPrivateState <WeaponPrivateState>(); var weaponAmmoCount = (int)itemWeaponPrivateState.AmmoCount; var weaponAmmoCapacity = itemWeaponProto.AmmoCapacity; isAmmoTypeChanged = false; var selectedProtoItemAmmo = weaponReloadingState.ProtoItemAmmo; var currentProtoItemAmmo = itemWeaponPrivateState.CurrentProtoItemAmmo; if (weaponAmmoCount > 0) { if (selectedProtoItemAmmo != currentProtoItemAmmo && weaponAmmoCount > 0) { // unload current ammo if (IsServer) { var targetContainers = SharedGetTargetContainersForCharacterAmmo(character, isForAmmoUnloading: true); var result = Server.Items.CreateItem( protoItem: currentProtoItemAmmo, new AggregatedItemsContainers(targetContainers), count: (ushort)weaponAmmoCount); if (!result.IsEverythingCreated) { // cannot unload current ammo - no space, try to unload to the ground result.Rollback(); var tile = Api.Server.World.GetTile(character.TilePosition); var groundContainer = ObjectGroundItemsContainer .ServerTryGetOrCreateGroundContainerAtTileOrNeighbors(character, tile); if (groundContainer is null) { // cannot unload current ammo to the ground - no free space around character NotificationSystem.ServerSendNotificationNoSpaceInInventory(character); return; } result = Server.Items.CreateItem( container: groundContainer, protoItem: currentProtoItemAmmo, count: (ushort)weaponAmmoCount); if (!result.IsEverythingCreated) { // cannot unload current ammo to the ground - no space in ground containers near the character result.Rollback(); NotificationSystem.ServerSendNotificationNoSpaceInInventory(character); return; } // notify player that there were not enough space in inventory so the items were dropped to the ground NotificationSystem.ServerSendNotificationNoSpaceInInventoryItemsDroppedToGround( character, result.ItemAmounts.First().Key?.ProtoItem); } } Logger.Info( $"Weapon ammo unloaded: {itemWeapon} -> {weaponAmmoCount} {currentProtoItemAmmo})", character); weaponAmmoCount = 0; itemWeaponPrivateState.SetAmmoCount(0); } else // if the same ammo type is loaded if (weaponAmmoCount == weaponAmmoCapacity) { // already completely loaded Logger.Info( $"Weapon reloading cancelled: {itemWeapon} - no reloading is required ({weaponAmmoCount}/{weaponAmmoCapacity} {selectedProtoItemAmmo})", character); return; } } else // if ammoCount == 0 if (selectedProtoItemAmmo is null && currentProtoItemAmmo is null) { Logger.Info( $"Weapon reloading cancelled: {itemWeapon} - already unloaded ({weaponAmmoCount}/{weaponAmmoCapacity})", character); return; } if (selectedProtoItemAmmo != null) { var selectedAmmoGroup = SharedGetCompatibleAmmoGroups(character, itemWeaponProto) .FirstOrDefault(g => g.Key == selectedProtoItemAmmo); if (selectedAmmoGroup is null) { Logger.Warning( $"Weapon reloading impossible: {itemWeapon} - no ammo of the required type ({selectedProtoItemAmmo})", character); return; } var ammoItems = SharedSelectAmmoItemsFromGroup(selectedAmmoGroup, ammoCountNeed: weaponAmmoCapacity - weaponAmmoCount); foreach (var request in ammoItems) { var itemAmmo = request.Item; Api.Assert(itemAmmo.ProtoItem == selectedProtoItemAmmo, "Sanity check"); int ammoToSubstract; var itemAmmoCount = itemAmmo.Count; if (itemAmmoCount == 0) { continue; } if (request.Count != itemAmmoCount) { if (request.Count < itemAmmoCount) { itemAmmoCount = request.Count; } else if (IsServer) { Logger.Warning( $"Trying to take more ammo to reload than player have: {itemAmmo} requested {request.Count}. Will reload as much as possible only.", character); } } if (weaponAmmoCount + itemAmmoCount >= weaponAmmoCapacity) { // there are more than enough ammo in that item stack to fully refill the weapon ammoToSubstract = weaponAmmoCapacity - weaponAmmoCount; weaponAmmoCount = weaponAmmoCapacity; } else { // consume full item stack ammoToSubstract = itemAmmoCount; weaponAmmoCount += itemAmmoCount; } // reduce ammo item count if (IsServer) { Server.Items.SetCount( itemAmmo, itemAmmo.Count - ammoToSubstract, byCharacter: character); } if (weaponAmmoCount == weaponAmmoCapacity) { // the weapon is fully reloaded, no need to subtract ammo from the next ammo items break; } } } if (currentProtoItemAmmo != selectedProtoItemAmmo) { // another ammo type selected itemWeaponPrivateState.CurrentProtoItemAmmo = selectedProtoItemAmmo; // reset weapon cache (it will be re-calculated on next fire processing) weaponState.WeaponCache = null; isAmmoTypeChanged = true; } if (weaponAmmoCount < 0 || weaponAmmoCount > weaponAmmoCapacity) { Logger.Error( "Something is completely wrong during reloading! Result ammo count is: " + weaponAmmoCount); weaponAmmoCount = 0; } itemWeaponPrivateState.SetAmmoCount((ushort)weaponAmmoCount); if (weaponAmmoCount == 0) { // weapon unloaded - and the log entry about this is already written (see above) return; } Logger.Info( $"Weapon reloaded: {itemWeapon} - ammo {weaponAmmoCount}/{weaponAmmoCapacity} {selectedProtoItemAmmo?.ToString() ?? "<no ammo>"}", character); if (IsServer) { ServerNotifyAboutReloading(character, weaponState, isFinished: true); } else { weaponState.ProtoWeapon.SoundPresetWeapon .PlaySound(WeaponSound.ReloadFinished, character, SoundConstants.VolumeWeapon); } }
public static void SharedUpdateReloading( WeaponState weaponState, ICharacter character, double deltaTime, out bool isReloadingNow) { var reloadingState = weaponState.WeaponReloadingState; if (reloadingState is null) { isReloadingNow = false; return; } if (reloadingState.Item != weaponState.ItemWeapon) { Logger.Info("Can reload only the current weapon. Reloading aborted"); SharedTryAbortReloading(character, reloadingState.Item); isReloadingNow = false; return; } if (IsServer && weaponState.SharedGetInputIsFiring() && weaponState.ItemWeapon is not null && weaponState.ItemWeapon.GetPrivateState <WeaponPrivateState>().AmmoCount > 0) { var shotsRemains = (long)weaponState.ServerLastClientReportedShotsDoneCount - weaponState.ShotsDone; if (shotsRemains > 0 && weaponState.ServerLastClientReportedShotsDoneCount > 0) { // sometimes the client reloading requests are received by the server // too early (before the server fired all the shots) so the server should fire them first //Logger.Dev("Server cannot reload while client is firing. Shot remains: " + shotsRemains); isReloadingNow = false; return; } } // process reloading reloadingState.SecondsToReloadRemains -= deltaTime; if (reloadingState.SecondsToReloadRemains > 0) { // need more time to reload isReloadingNow = true; return; } // reloaded reloadingState.SecondsToReloadRemains = 0; SharedProcessWeaponReload(character, weaponState, out var isAmmoTypeChanged); if (isAmmoTypeChanged) { weaponState.ClearFiringStateData(); //Api.Logger.Dev("Reset ServerLastClientReportedShotsDoneCount. Last value: " // + weaponState.ServerLastClientReportedShotsDoneCount); } weaponState.FirePatternCooldownSecondsRemains = 0; weaponState.IsIdleAutoReloadingAllowed = true; isReloadingNow = false; }