diff --git a/src/main/java/emu/grasscutter/data/binout/AbilityData.java b/src/main/java/emu/grasscutter/data/binout/AbilityData.java index 65ddb3abe..2a0e40425 100644 --- a/src/main/java/emu/grasscutter/data/binout/AbilityData.java +++ b/src/main/java/emu/grasscutter/data/binout/AbilityData.java @@ -1,13 +1,67 @@ package emu.grasscutter.data.binout; +import emu.grasscutter.data.binout.AbilityModifier.AbilityModifierAction; +import emu.grasscutter.game.ability.AbilityLocalIdGenerator; + +import java.util.HashMap; import java.util.Map; +import static emu.grasscutter.game.ability.AbilityLocalIdGenerator.*; + public class AbilityData { public String abilityName; public Map modifiers; public boolean isDynamicAbility; public Map abilitySpecials; + public AbilityModifierAction[] onAdded; + // abilityMixins // onAbilityStart // onKill + + public final Map localIdToAction + = new HashMap<>(); + + private boolean _initialized = false; + public void initialize() { + if(_initialized) return; + _initialized = true; + + if(modifiers == null) return; + + var _modifiers = modifiers.values().toArray(new AbilityModifier[0]); + var modifierIndex = 0; for (var modifier : _modifiers) { + long configIndex = 0L; + this.initializeActionSubCategory(modifierIndex, configIndex++, modifier.onAdded, localIdToAction); + this.initializeActionSubCategory(modifierIndex, configIndex++, modifier.onRemoved, localIdToAction); + this.initializeActionSubCategory(modifierIndex, configIndex++, modifier.onBeingHit, localIdToAction); + this.initializeActionSubCategory(modifierIndex, configIndex++, modifier.onAttackLanded, localIdToAction); + this.initializeActionSubCategory(modifierIndex, configIndex++, modifier.onHittingOther, localIdToAction); + this.initializeActionSubCategory(modifierIndex, configIndex++, modifier.onThinkInterval, localIdToAction); + this.initializeActionSubCategory(modifierIndex, configIndex++, modifier.onKill, localIdToAction); + this.initializeActionSubCategory(modifierIndex, configIndex++, modifier.onCrash, localIdToAction); + this.initializeActionSubCategory(modifierIndex, configIndex++, modifier.onAvatarIn, localIdToAction); + this.initializeActionSubCategory(modifierIndex, configIndex++, modifier.onAvatarOut, localIdToAction); + this.initializeActionSubCategory(modifierIndex, configIndex++, modifier.onReconnect, localIdToAction); + this.initializeActionSubCategory(modifierIndex, configIndex++, modifier.onChangeAuthority, localIdToAction); + this.initializeActionSubCategory(modifierIndex, configIndex++, modifier.onVehicleIn, localIdToAction); + this.initializeActionSubCategory(modifierIndex, configIndex++, modifier.onVehicleOut, localIdToAction); + this.initializeActionSubCategory(modifierIndex, configIndex++, modifier.onZoneEnter, localIdToAction); + this.initializeActionSubCategory(modifierIndex, configIndex++, modifier.onZoneExit, localIdToAction); + this.initializeActionSubCategory(modifierIndex, configIndex++, modifier.onHeal, localIdToAction); + this.initializeActionSubCategory(modifierIndex, configIndex++, modifier.onBeingHealed, localIdToAction); + + modifierIndex++; + } + } + + private void initializeActionSubCategory(long modifierIndex, long configIndex, AbilityModifierAction[] actions, Map localIdToAction) { + if(actions == null) return; + + var generator = new AbilityLocalIdGenerator(ConfigAbilitySubContainerType.MODIFIER_ACTION); + generator.modifierIndex = modifierIndex; + generator.configIndex = configIndex; + + generator.initializeActionLocalIds(actions, localIdToAction); + } } diff --git a/src/main/java/emu/grasscutter/data/binout/AbilityModifier.java b/src/main/java/emu/grasscutter/data/binout/AbilityModifier.java index c4f713d43..296e9a3b1 100644 --- a/src/main/java/emu/grasscutter/data/binout/AbilityModifier.java +++ b/src/main/java/emu/grasscutter/data/binout/AbilityModifier.java @@ -1,270 +1,122 @@ -package emu.grasscutter.data.binout; - -import com.google.gson.annotations.SerializedName; -import emu.grasscutter.data.common.DynamicFloat; -import java.io.Serializable; - -public class AbilityModifier implements Serializable { - private static final long serialVersionUID = -2001232313615923575L; - - @SerializedName( - value = "onAdded", - alternate = {"KCICDEJLIJD"}) - public AbilityModifierAction[] onAdded; - - @SerializedName( - value = "onThinkInterval", - alternate = {"PBDDACFFPOE"}) - public AbilityModifierAction[] onThinkInterval; - - public AbilityModifierAction[] onRemoved; - public DynamicFloat duration = DynamicFloat.ZERO; - - public static class AbilityModifierAction { - @SerializedName("$type") - public Type type; - - public String target; - - @SerializedName(value = "amount", alternate = "PDLLIFICICJ") - public DynamicFloat amount = DynamicFloat.ZERO; - - public DynamicFloat amountByCasterAttackRatio = DynamicFloat.ZERO; - public DynamicFloat amountByCasterCurrentHPRatio = DynamicFloat.ZERO; - public DynamicFloat amountByCasterMaxHPRatio = DynamicFloat.ZERO; - public DynamicFloat amountByGetDamage = DynamicFloat.ZERO; - public DynamicFloat amountByTargetCurrentHPRatio = DynamicFloat.ZERO; - public DynamicFloat amountByTargetMaxHPRatio = DynamicFloat.ZERO; - - @SerializedName(value = "ignoreAbilityProperty", alternate = "HHFGADCJJDI") - public boolean ignoreAbilityProperty; - - public String modifierName; - - public enum Type { - ActCameraRadialBlur, - ActCameraShake, - AddAvatarSkillInfo, - AddChargeBarValue, - AddClimateMeter, - AddElementDurability, - AddGlobalValue, - AddGlobalValueToTarget, - AddRegionalPlayVarValue, - ApplyModifier, - AttachAbilityStateResistance, - AttachBulletAimPoint, - AttachEffect, - AttachEffectFirework, - AttachElementTypeResistance, - AttachModifier, - AttachUIEffect, - AvatarCameraParam, - AvatarEnterCameraShot, - AvatarEnterFocus, - AvatarEnterViewBias, - AvatarExitCameraShot, - AvatarExitClimb, - AvatarExitFocus, - AvatarExitViewBias, - AvatarShareCDSkillStart, - AvatarSkillStart, - BroadcastNeuronStimulate, - CalcDvalinS04RebornPoint, - CallLuaTask, - ChangeEnviroWeather, - ChangeFollowDampTime, - ChangeGadgetUIInteractHint, - ChangePlayMode, - ChangeTag, - ChangeUGCRayTag, - ClearEndura, - ClearGlobalPos, - ClearGlobalValue, - ClearLocalGadgets, - ClearLockTarget, - ClearPos, - ConfigAbilityAction, - ControlEmotion, - CopyGlobalValue, - CreateGadget, - CreateMovingPlatform, - CreateTile, - DamageByAttackValue, - DebugLog, - DestroyTile, - DoBlink, - DoTileAction, - DoWatcherSystemAction, - DoWidgetSystemAction, - DropSubfield, - DummyAction, - DungeonFogEffects, - ElementAttachForActivityGacha, - EnableAIStealthy, - EnableAfterImage, - EnableAvatarFlyStateTrail, - EnableAvatarMoveOnWater, - EnableBulletCollisionPluginTrigger, - EnableGadgetIntee, - EnableHeadControl, - EnableHitBoxByName, - EnableMainInterface, - EnablePartControl, - EnablePositionSynchronization, - EnablePushColliderName, - EnableRocketJump, - EnableSceneTransformByName, - EnterCameraLock, - EntityDoSkill, - EquipAffixStart, - ExecuteGadgetLua, - FireAISoundEvent, - FireChargeBarEffect, - FireEffect, - FireEffectFirework, - FireEffectForStorm, - FireFishingEvent, - FireHitEffect, - FireSubEmitterEffect, - FireUIEffect, - FixedMonsterRushMove, - ForceAirStateFly, - ForceEnableShakeOffButton, - GenerateElemBall, - GetFightProperty, - GetInteractIdToGlobalValue, - GetPos, - HealHP, - HideUIBillBoard, - IgnoreMoveColToRockCol, - KillGadget, - KillPlayEntity, - KillSelf, - KillServerGadget, - LoseHP, - ModifyAvatarSkillCD, - ModifyVehicleSkillCD, - PlayEmoSync, - Predicated, - PushDvalinS01Process, - PushInterActionByConfigPath, - PushPos, - Randomed, - ReTriggerAISkillInitialCD, - RefreshUICombatBarLayout, - RegisterAIActionPoint, - ReleaseAIActionPoint, - RemoveAvatarSkillInfo, - RemoveModifier, - RemoveModifierByAbilityStateResistanceID, - RemoveServerBuff, - RemoveUniqueModifier, - RemoveVelocityForce, - Repeated, - ResetAIAttackTarget, - ResetAIResistTauntLevel, - ResetAIThreatBroadcastRange, - ResetAnimatorTrigger, - ReviveDeadAvatar, - ReviveElemEnergy, - ReviveStamina, - SectorCityManeuver, - SendEffectTrigger, - SendEffectTriggerToLineEffect, - SendEvtElectricCoreMoveEnterP1, - SendEvtElectricCoreMoveInterrupt, - ServerLuaCall, - ServerLuaTriggerEvent, - ServerMonsterLog, - SetAIHitFeeling, - SetAISkillCDAvailableNow, - SetAISkillCDMultiplier, - SetAISkillGCD, - SetAnimatorBool, - SetAnimatorFloat, - SetAnimatorInt, - SetAnimatorTrigger, - SetAvatarCanShakeOff, - SetAvatarHitBuckets, - SetCanDieImmediately, - SetChargeBarValue, - SetDvalinS01FlyState, - SetEmissionScaler, - SetEntityScale, - SetExtraAbilityEnable, - SetExtraAbilityState, - SetGlobalDir, - SetGlobalPos, - SetGlobalValue, - SetGlobalValueByTargetDistance, - SetGlobalValueToOverrideMap, - SetKeepInAirVelocityForce, - SetMaterialParamFloatByTransform, - SetNeuronEnable, - SetOverrideMapValue, - SetPartControlTarget, - SetPoseBool, - SetPoseFloat, - SetPoseInt, - SetRandomOverrideMapValue, - SetRegionalPlayVarValue, - SetSelfAttackTarget, - SetSkillAnchor, - SetSpecialCamera, - SetSurroundAnchor, - SetSystemValueToOverrideMap, - SetTargetNumToGlobalValue, - SetUICombatBarAsh, - SetUICombatBarSpark, - SetVelocityIgnoreAirGY, - SetWeaponAttachPointRealName, - SetWeaponBindState, - ShowExtraAbility, - ShowProgressBarAction, - ShowReminder, - ShowScreenEffect, - ShowTextMap, - ShowUICombatBar, - StartDither, - SumTargetWeightToSelfGlobalValue, - Summon, - SyncToStageScript, - TriggerAbility, - TriggerAttackEvent, - TriggerAttackTargetMapEvent, - TriggerAudio, - TriggerAuxWeaponTrans, - TriggerBullet, - TriggerCreateGadgetToEquipPart, - TriggerDropEquipParts, - TriggerFaceAnimation, - TriggerGadgetInteractive, - TriggerHideWeapon, - TriggerSetCastShadow, - TriggerSetPassThrough, - TriggerSetRenderersEnable, - TriggerSetShadowRamp, - TriggerSetVisible, - TriggerTaunt, - TriggerThrowEquipPart, - TriggerUGCGadgetMove, - TryFindBlinkPoint, - TryFindBlinkPointByBorn, - TryTriggerPlatformStartMove, - TurnDirection, - TurnDirectionToPos, - UpdateReactionDamage, - UseSkillEliteSet, - WidgetSkillStart - } - } - - // The following should be implemented into DynamicFloat if older resource formats need to be - // supported - // public static class AbilityModifierValue { - // public boolean isFormula; - // public boolean isDynamic; - // public String dynamicKey; - // } -} +package emu.grasscutter.data.binout; + +import java.io.Serializable; + +import com.google.gson.annotations.SerializedName; + +import emu.grasscutter.data.common.DynamicFloat; +import emu.grasscutter.game.props.ElementType; + +public class AbilityModifier implements Serializable { + private static final long serialVersionUID = -2001232313615923575L; + + @SerializedName(value="onAdded", alternate={"KCICDEJLIJD"}) + public AbilityModifierAction[] onAdded; + @SerializedName(value="onThinkInterval", alternate={"PBDDACFFPOE"}) + public AbilityModifierAction[] onThinkInterval; + public AbilityModifierAction[] onRemoved; + public AbilityModifierAction[] onBeingHit; + public AbilityModifierAction[] onAttackLanded; + public AbilityModifierAction[] onHittingOther; + public AbilityModifierAction[] onKill; + public AbilityModifierAction[] onCrash; + public AbilityModifierAction[] onAvatarIn; + public AbilityModifierAction[] onAvatarOut; + public AbilityModifierAction[] onReconnect; + public AbilityModifierAction[] onChangeAuthority; + public AbilityModifierAction[] onVehicleIn; + public AbilityModifierAction[] onVehicleOut; + public AbilityModifierAction[] onZoneEnter; + public AbilityModifierAction[] onZoneExit; + public AbilityModifierAction[] onHeal; + public AbilityModifierAction[] onBeingHealed; + public DynamicFloat duration = DynamicFloat.ZERO; + public String stacking; + + public ElementType elementType; + public DynamicFloat elementDurability = DynamicFloat.ZERO; + + public static class AbilityModifierAction implements Serializable { + public enum Type { + ActCameraRadialBlur, ActCameraShake, AddAvatarSkillInfo, AddChargeBarValue, + AddClimateMeter, AddElementDurability, AddGlobalValue, AddGlobalValueToTarget, + AddRegionalPlayVarValue, ApplyModifier, AttachAbilityStateResistance, AttachBulletAimPoint, + AttachEffect, AttachEffectFirework, AttachElementTypeResistance, AttachModifier, + AttachUIEffect, AvatarCameraParam, AvatarEnterCameraShot, AvatarEnterFocus, + AvatarEnterViewBias, AvatarExitCameraShot, AvatarExitClimb, AvatarExitFocus, + AvatarExitViewBias, AvatarShareCDSkillStart, AvatarSkillStart, BroadcastNeuronStimulate, + CalcDvalinS04RebornPoint, CallLuaTask, ChangeEnviroWeather, ChangeFollowDampTime, + ChangeGadgetUIInteractHint, ChangePlayMode, ChangeTag, ChangeUGCRayTag, + ClearEndura, ClearGlobalPos, ClearGlobalValue, ClearLocalGadgets, + ClearLockTarget, ClearPos, ConfigAbilityAction, ControlEmotion, + CopyGlobalValue, CreateGadget, CreateMovingPlatform, CreateTile, + DamageByAttackValue, DebugLog, DestroyTile, DoBlink, + DoTileAction, DoWatcherSystemAction, DoWidgetSystemAction, DropSubfield, + DummyAction, DungeonFogEffects, ElementAttachForActivityGacha, EnableAIStealthy, + EnableAfterImage, EnableAvatarFlyStateTrail, EnableAvatarMoveOnWater, EnableBulletCollisionPluginTrigger, + EnableGadgetIntee, EnableHeadControl, EnableHitBoxByName, EnableMainInterface, + EnablePartControl, EnablePositionSynchronization, EnablePushColliderName, EnableRocketJump, + EnableSceneTransformByName, EnterCameraLock, EntityDoSkill, EquipAffixStart, + ExecuteGadgetLua, FireAISoundEvent, FireChargeBarEffect, FireEffect, + FireEffectFirework, FireEffectForStorm, FireFishingEvent, FireHitEffect, + FireSubEmitterEffect, FireUIEffect, FixedMonsterRushMove, ForceAirStateFly, + ForceEnableShakeOffButton, GenerateElemBall, GetFightProperty, GetInteractIdToGlobalValue, + GetPos, HealHP, HideUIBillBoard, IgnoreMoveColToRockCol, + KillGadget, KillPlayEntity, KillSelf, KillServerGadget, + LoseHP, ModifyAvatarSkillCD, ModifyVehicleSkillCD, PlayEmoSync, + Predicated, PushDvalinS01Process, PushInterActionByConfigPath, PushPos, + Randomed, ReTriggerAISkillInitialCD, RefreshUICombatBarLayout, RegisterAIActionPoint, + ReleaseAIActionPoint, RemoveAvatarSkillInfo, RemoveModifier, RemoveModifierByAbilityStateResistanceID, + RemoveServerBuff, RemoveUniqueModifier, RemoveVelocityForce, Repeated, + ResetAIAttackTarget, ResetAIResistTauntLevel, ResetAIThreatBroadcastRange, ResetAnimatorTrigger, + ReviveDeadAvatar, ReviveElemEnergy, ReviveStamina, SectorCityManeuver, + SendEffectTrigger, SendEffectTriggerToLineEffect, SendEvtElectricCoreMoveEnterP1, SendEvtElectricCoreMoveInterrupt, + ServerLuaCall, ServerLuaTriggerEvent, ServerMonsterLog, SetAIHitFeeling, + SetAISkillCDAvailableNow, SetAISkillCDMultiplier, SetAISkillGCD, SetAnimatorBool, + SetAnimatorFloat, SetAnimatorInt, SetAnimatorTrigger, SetAvatarCanShakeOff, + SetAvatarHitBuckets, SetCanDieImmediately, SetChargeBarValue, SetDvalinS01FlyState, + SetEmissionScaler, SetEntityScale, SetExtraAbilityEnable, SetExtraAbilityState, + SetGlobalDir, SetGlobalPos, SetGlobalValue, SetGlobalValueByTargetDistance, + SetGlobalValueToOverrideMap, SetKeepInAirVelocityForce, SetMaterialParamFloatByTransform, SetNeuronEnable, + SetOverrideMapValue, SetPartControlTarget, SetPoseBool, SetPoseFloat, + SetPoseInt, SetRandomOverrideMapValue, SetRegionalPlayVarValue, SetSelfAttackTarget, + SetSkillAnchor, SetSpecialCamera, SetSurroundAnchor, SetSystemValueToOverrideMap, + SetTargetNumToGlobalValue, SetUICombatBarAsh, SetUICombatBarSpark, SetVelocityIgnoreAirGY, + SetWeaponAttachPointRealName, SetWeaponBindState, ShowExtraAbility, ShowProgressBarAction, + ShowReminder, ShowScreenEffect, ShowTextMap, ShowUICombatBar, + StartDither, SumTargetWeightToSelfGlobalValue, Summon, SyncToStageScript, + TriggerAbility, TriggerAttackEvent, TriggerAttackTargetMapEvent, TriggerAudio, + TriggerAuxWeaponTrans, TriggerBullet, TriggerCreateGadgetToEquipPart, TriggerDropEquipParts, + TriggerFaceAnimation, TriggerGadgetInteractive, TriggerHideWeapon, TriggerSetCastShadow, + TriggerSetPassThrough, TriggerSetRenderersEnable, TriggerSetShadowRamp, TriggerSetVisible, + TriggerTaunt, TriggerThrowEquipPart, TriggerUGCGadgetMove, TryFindBlinkPoint, + TryFindBlinkPointByBorn, TryTriggerPlatformStartMove, TurnDirection, TurnDirectionToPos, + UpdateReactionDamage, UseSkillEliteSet, WidgetSkillStart; + } + @SerializedName("$type") + public Type type; + public String target; + @SerializedName(value = "amount", alternate = "PDLLIFICICJ") + public DynamicFloat amount = DynamicFloat.ZERO; + public DynamicFloat amountByCasterAttackRatio = DynamicFloat.ZERO; + public DynamicFloat amountByCasterCurrentHPRatio = DynamicFloat.ZERO; + public DynamicFloat amountByCasterMaxHPRatio = DynamicFloat.ZERO; + public DynamicFloat amountByGetDamage = DynamicFloat.ZERO; + public DynamicFloat amountByTargetCurrentHPRatio = DynamicFloat.ZERO; + public DynamicFloat amountByTargetMaxHPRatio = DynamicFloat.ZERO; + @SerializedName(value = "ignoreAbilityProperty", alternate = "HHFGADCJJDI") + public boolean ignoreAbilityProperty; + public String modifierName; + + public int param1; + public int param2; + public int param3; + } + + //The following should be implemented into DynamicFloat if older resource formats need to be supported + // public static class AbilityModifierValue { + // public boolean isFormula; + // public boolean isDynamic; + // public String dynamicKey; + // } +} diff --git a/src/main/java/emu/grasscutter/data/server/Grid.java b/src/main/java/emu/grasscutter/data/server/Grid.java index 3c4ed57a0..bf332b8ec 100644 --- a/src/main/java/emu/grasscutter/data/server/Grid.java +++ b/src/main/java/emu/grasscutter/data/server/Grid.java @@ -1,48 +1,63 @@ package emu.grasscutter.data.server; +import com.github.davidmoten.rtreemulti.RTree; +import com.github.davidmoten.rtreemulti.geometry.Geometry; import emu.grasscutter.Grasscutter; +import emu.grasscutter.scripts.SceneIndexManager; import emu.grasscutter.utils.GridPosition; import emu.grasscutter.utils.Position; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Set; + +import java.util.*; public class Grid { - public Map> grid; - public Map> gridMap = new LinkedHashMap<>(); + public transient RTree>, Geometry> gridOptimized = null; + private transient Set nearbyGroups = new HashSet<>(100); - /** Loads the correct grid map. */ - public void load() { - this.grid.forEach((position, groups) -> this.gridMap.put(new GridPosition(position), groups)); + public Map> grid = new LinkedHashMap<>(); + + /** + * Creates an optimized cache of the grid. + */ + private void optimize() { + if (this.gridOptimized == null) { + var gridValues = new ArrayList>>(); + this.grid.forEach((k, v) -> gridValues.add(new AbstractMap.SimpleEntry<>(k, v))); + this.gridOptimized = SceneIndexManager.buildIndex(2, gridValues, entry -> entry.getKey().toPoint()); + } } /** * @return The correctly loaded grid map. */ public Map> getGrid() { - return this.gridMap; + return this.grid; } public Set getNearbyGroups(int vision_level, Position position) { + this.optimize(); // Check to see if the grid is optimized. + int width = Grasscutter.getConfig().server.game.visionOptions[vision_level].gridWidth; int vision_range = Grasscutter.getConfig().server.game.visionOptions[vision_level].visionRange; int vision_range_grid = vision_range / width; GridPosition pos = new GridPosition(position, width); - Set nearbyGroups = new HashSet<>(); - // construct a nearby pisition list, add 1 more because a player can be in an edge case, this + this.nearbyGroups.clear(); + // construct a nearby position list, add 1 more because a player can be in an edge case, this // should not affect much the loading - for (int x = 0; x < vision_range_grid + 1; x++) { - for (int z = 0; z < vision_range_grid + 1; z++) { - nearbyGroups.addAll(gridMap.getOrDefault(pos.addClone(x, z), new HashSet<>())); - nearbyGroups.addAll(gridMap.getOrDefault(pos.addClone(-x, z), new HashSet<>())); - nearbyGroups.addAll(gridMap.getOrDefault(pos.addClone(x, -z), new HashSet<>())); - nearbyGroups.addAll(gridMap.getOrDefault(pos.addClone(-x, -z), new HashSet<>())); - } - } + // var nearbyGroups = new HashSet(); + // for (int x = 0; x < vision_range_grid + 1; x++) { + // for (int z = 0; z < vision_range_grid + 1; z++) { + // nearbyGroups.addAll(gridMap.getOrDefault(pos.addClone(x, z), new HashSet<>())); + // nearbyGroups.addAll(gridMap.getOrDefault(pos.addClone(-x, z), new HashSet<>())); + // nearbyGroups.addAll(gridMap.getOrDefault(pos.addClone(x, -z), new HashSet<>())); + // nearbyGroups.addAll(gridMap.getOrDefault(pos.addClone(-x, -z), new HashSet<>())); + // } + // } - return nearbyGroups; + // Construct a list of nearby groups. + SceneIndexManager.queryNeighbors(gridOptimized, pos.toDoubleArray(), vision_range_grid + 1) + .forEach(e -> nearbyGroups.addAll(e.getValue())); + return this.nearbyGroups; } } diff --git a/src/main/java/emu/grasscutter/game/ability/Ability.java b/src/main/java/emu/grasscutter/game/ability/Ability.java new file mode 100644 index 000000000..f9f0f14d6 --- /dev/null +++ b/src/main/java/emu/grasscutter/game/ability/Ability.java @@ -0,0 +1,47 @@ +package emu.grasscutter.game.ability; + +import java.util.HashMap; +import java.util.Map; + +import emu.grasscutter.data.binout.AbilityData; +import emu.grasscutter.game.entity.GameEntity; +import emu.grasscutter.server.event.entity.EntityDamageEvent; +import emu.grasscutter.utils.Utils; +import lombok.Getter; + +public final class Ability { + @Getter private AbilityData data; + @Getter private GameEntity owner; + + @Getter private AbilityManager manager; + @Getter private Map modifiers + = new HashMap<>(); + + @Getter private int hash; + + public Ability(AbilityData data, GameEntity owner) { + this.data = data; + this.owner = owner; + this.manager = owner.getScene().getWorld().getHost().getAbilityManager(); + this.hash = Utils.abilityHash(data.abilityName); + + data.initialize(); + } + + public void onAdded() { + if (this.data.onAdded == null) return; + for (var action : data.onAdded) { + this.manager.executeAction(this, action); + } + } + + public void onRemoved() { + var tempModifiers = new HashMap<>(this.modifiers); + tempModifiers.values().forEach(AbilityModifierController::onRemoved); + } + + public void onBeingHit(EntityDamageEvent event) { + var tempModifiers = new HashMap<>(this.modifiers); + tempModifiers.values().forEach(m -> m.onBeingHit(event)); + } +} diff --git a/src/main/java/emu/grasscutter/game/ability/AbilityAction.java b/src/main/java/emu/grasscutter/game/ability/AbilityAction.java new file mode 100644 index 000000000..508dffbed --- /dev/null +++ b/src/main/java/emu/grasscutter/game/ability/AbilityAction.java @@ -0,0 +1,11 @@ +package emu.grasscutter.game.ability; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import emu.grasscutter.data.binout.AbilityModifier.AbilityModifierAction; + +@Retention(RetentionPolicy.RUNTIME) +public @interface AbilityAction { + AbilityModifierAction.Type value(); +} diff --git a/src/main/java/emu/grasscutter/game/ability/AbilityActionHandler.java b/src/main/java/emu/grasscutter/game/ability/AbilityActionHandler.java new file mode 100644 index 000000000..89b3b62a0 --- /dev/null +++ b/src/main/java/emu/grasscutter/game/ability/AbilityActionHandler.java @@ -0,0 +1,7 @@ +package emu.grasscutter.game.ability; + +import emu.grasscutter.data.binout.AbilityModifier.AbilityModifierAction; + +public abstract class AbilityActionHandler { + public abstract boolean execute(Ability ability, AbilityModifierAction action); +} diff --git a/src/main/java/emu/grasscutter/game/ability/AbilityLocalIdGenerator.java b/src/main/java/emu/grasscutter/game/ability/AbilityLocalIdGenerator.java new file mode 100644 index 000000000..51eb35298 --- /dev/null +++ b/src/main/java/emu/grasscutter/game/ability/AbilityLocalIdGenerator.java @@ -0,0 +1,65 @@ +package emu.grasscutter.game.ability; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.data.binout.AbilityModifier.AbilityModifierAction; +import lombok.AllArgsConstructor; + +import java.util.Map; + +public final class AbilityLocalIdGenerator { + public ConfigAbilitySubContainerType type; + public long modifierIndex = 0; + public long configIndex = 0; + public long mixinIndex = 0; + private long actionIndex = 0; + + public AbilityLocalIdGenerator(ConfigAbilitySubContainerType type) + { + this.type = type; + } + + public void initializeActionLocalIds(AbilityModifierAction[] actions, Map localIdToAction) + { + if (actions == null) return; + + actionIndex = 0; + for (var action : actions) { + actionIndex++; + long id = GetLocalId(); + localIdToAction.put((int) id, action); + } + + actionIndex = 0; + } + + public long GetLocalId() { + switch (type) { + case ACTION -> { + return type.value + (configIndex << 3) + (actionIndex << 9); + } + case MIXIN -> { + return type.value + (mixinIndex << 3) + (configIndex << 9) + (actionIndex << 15); + } + case MODIFIER_ACTION -> { + return type.value + (modifierIndex << 3) + (configIndex << 9) + (actionIndex << 15); + } + case MODIFIER_MIXIN -> { + return type.value + (modifierIndex << 3) + (mixinIndex << 9) + (configIndex << 15) + (actionIndex << 21); + } + case NONE -> Grasscutter.getLogger().error("Ability local id generator using NONE type."); + } + + return -1; + } + + @AllArgsConstructor + public enum ConfigAbilitySubContainerType { + NONE(0), + ACTION(1), + MIXIN(2), + MODIFIER_ACTION(3), + MODIFIER_MIXIN(4); + + public final long value; + } +} diff --git a/src/main/java/emu/grasscutter/game/ability/AbilityManager.java b/src/main/java/emu/grasscutter/game/ability/AbilityManager.java index 82cf0de04..fec0df009 100644 --- a/src/main/java/emu/grasscutter/game/ability/AbilityManager.java +++ b/src/main/java/emu/grasscutter/game/ability/AbilityManager.java @@ -1,7 +1,9 @@ package emu.grasscutter.game.ability; import com.google.protobuf.InvalidProtocolBufferException; +import emu.grasscutter.Grasscutter; import emu.grasscutter.data.GameData; +import emu.grasscutter.data.binout.AbilityData; import emu.grasscutter.data.binout.AbilityModifier.AbilityModifierAction; import emu.grasscutter.data.binout.AbilityModifierEntry; import emu.grasscutter.game.entity.EntityGadget; @@ -10,30 +12,89 @@ import emu.grasscutter.game.entity.gadget.GadgetGatherObject; import emu.grasscutter.game.player.BasePlayerManager; import emu.grasscutter.game.player.Player; import emu.grasscutter.game.quest.enums.QuestContent; -import emu.grasscutter.net.proto.AbilityInvokeEntryHeadOuterClass.AbilityInvokeEntryHead; import emu.grasscutter.net.proto.AbilityInvokeEntryOuterClass.AbilityInvokeEntry; import emu.grasscutter.net.proto.AbilityMetaModifierChangeOuterClass.AbilityMetaModifierChange; import emu.grasscutter.net.proto.AbilityMetaReInitOverrideMapOuterClass.AbilityMetaReInitOverrideMap; import emu.grasscutter.net.proto.AbilityMixinCostStaminaOuterClass.AbilityMixinCostStamina; import emu.grasscutter.net.proto.AbilityScalarValueEntryOuterClass.AbilityScalarValueEntry; import emu.grasscutter.net.proto.ModifierActionOuterClass.ModifierAction; +import io.netty.util.concurrent.FastThreadLocalThread; import lombok.Getter; +import org.reflections.Reflections; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; public final class AbilityManager extends BasePlayerManager { - HealAbilityManager healAbilityManager; + public static final ExecutorService eventExecutor; + + static { + eventExecutor = new ThreadPoolExecutor(4, 4, + 60, TimeUnit.SECONDS, new LinkedBlockingDeque<>(1000), + FastThreadLocalThread::new, new ThreadPoolExecutor.AbortPolicy()); + } + + private final HealAbilityManager healAbilityManager; + private final Map actionHandlers; @Getter private boolean abilityInvulnerable = false; public AbilityManager(Player player) { super(player); + this.healAbilityManager = new HealAbilityManager(player); + this.actionHandlers = new HashMap<>(); + + this.registerHandlers(); + } + + /** + * Registers all present ability handlers. + */ + private void registerHandlers() { + var reflections = new Reflections("emu.grasscutter.game.ability.actions"); + var handlerClasses = reflections.getSubTypesOf(AbilityActionHandler.class); + + for (var obj : handlerClasses) { + try { + if (obj.isAnnotationPresent(AbilityAction.class)) { + AbilityModifierAction.Type abilityAction = obj.getAnnotation(AbilityAction.class).value(); + actionHandlers.put(abilityAction, obj.getDeclaredConstructor().newInstance()); + } else { + return; + } + } catch (Exception e) { + Grasscutter.getLogger().error("Unable to register handler.", e); + } + } + } + + public void executeAction(Ability ability, AbilityModifierAction action) { + var handler = actionHandlers.get(action.type); + + if (handler == null || ability == null) { + Grasscutter.getLogger().debug("Could not execute ability action {} at {}", action.type, ability); + return; + } + + eventExecutor.submit(() -> { + if (!handler.execute(ability, action)) { + Grasscutter.getLogger().debug("exec ability action failed {} at {}", action.type, ability); + } + }); } public void onAbilityInvoke(AbilityInvokeEntry invoke) throws Exception { this.healAbilityManager.healHandler(invoke); - // Grasscutter.getLogger().info(invoke.getArgumentType() + " (" + invoke.getArgumentTypeValue() - // + "): " + Utils.bytesToHex(invoke.toByteArray())); + if (invoke.getEntityId() == 67109298) { + Grasscutter.getLogger().info(invoke.getArgumentType() + " (" + invoke.getArgumentTypeValue() + "): " + invoke.getEntityId()); + } + switch (invoke.getArgumentType()) { case ABILITY_INVOKE_ARGUMENT_META_OVERRIDE_PARAM -> this.handleOverrideParam(invoke); case ABILITY_INVOKE_ARGUMENT_META_REINIT_OVERRIDEMAP -> this.handleReinitOverrideMap(invoke); @@ -41,6 +102,9 @@ public final class AbilityManager extends BasePlayerManager { case ABILITY_INVOKE_ARGUMENT_MIXIN_COST_STAMINA -> this.handleMixinCostStamina(invoke); case ABILITY_INVOKE_ARGUMENT_ACTION_GENERATE_ELEM_BALL -> this.handleGenerateElemBall(invoke); case ABILITY_INVOKE_ARGUMENT_META_GLOBAL_FLOAT_VALUE -> this.handleGlobalFloatValue(invoke); + case ABILITY_INVOKE_ARGUMENT_META_MODIFIER_DURABILITY_CHANGE -> this.handleModifierDurabilityChange(invoke); + case ABILITY_INVOKE_ARGUMENT_META_ADD_NEW_ABILITY -> this.handleAddNewAbility(invoke); + case ABILITY_INVOKE_ARGUMENT_NONE -> this.handleInvoke(invoke); default -> {} } } @@ -97,41 +161,75 @@ public final class AbilityManager extends BasePlayerManager { this.abilityInvulnerable = false; } + /** + * Handles an ability invoke. + * + * @param invoke The invocation. + */ + private void handleInvoke(AbilityInvokeEntry invoke) { + var entity = this.player.getScene().getEntityById(invoke.getEntityId()); + if (entity == null) { + return; + } + + var head = invoke.getHead(); + Grasscutter.getLogger().debug("{} {} {}", head.getInstancedAbilityId(), entity.getInstanceToHash(), head.getLocalId()); + + var hash = entity.getInstanceToHash().get(head.getInstancedAbilityId()); + if (hash == null) { + var abilities = entity.getAbilities().values().toArray(new Ability[0]); + + if(head.getInstancedAbilityId() <= abilities.length) { + var ability = abilities[head.getInstancedAbilityId() - 1]; + Grasscutter.getLogger().warn("-> {}", ability.getData().localIdToAction); + var action = ability.getData().localIdToAction.get(head.getLocalId()); + if(action != null) ability.getManager().executeAction(ability, action); + } + + return; + } + + var stream = entity.getAbilities().values().stream() + .filter(a -> a.getHash() == hash || + a.getData().abilityName == entity.getInstanceToName().get(head.getInstancedAbilityId())); + stream.forEach(ability -> { + var action = ability.getData().localIdToAction.get(head.getLocalId()); + if(action != null) ability.getManager().executeAction(ability, action); + }); + } + private void handleOverrideParam(AbilityInvokeEntry invoke) throws Exception { - GameEntity entity = this.player.getScene().getEntityById(invoke.getEntityId()); + var entity = this.player.getScene().getEntityById(invoke.getEntityId()); if (entity == null) { return; } - AbilityScalarValueEntry entry = AbilityScalarValueEntry.parseFrom(invoke.getAbilityData()); - + var entry = AbilityScalarValueEntry.parseFrom(invoke.getAbilityData()); entity.getMetaOverrideMap().put(entry.getKey().getStr(), entry.getFloatValue()); } private void handleReinitOverrideMap(AbilityInvokeEntry invoke) throws Exception { - GameEntity entity = this.player.getScene().getEntityById(invoke.getEntityId()); + var entity = this.player.getScene().getEntityById(invoke.getEntityId()); if (entity == null) { return; } - AbilityMetaReInitOverrideMap map = - AbilityMetaReInitOverrideMap.parseFrom(invoke.getAbilityData()); - - for (AbilityScalarValueEntry entry : map.getOverrideMapList()) { + var map = AbilityMetaReInitOverrideMap.parseFrom(invoke.getAbilityData()); + for (var entry : map.getOverrideMapList()) { entity.getMetaOverrideMap().put(entry.getKey().getStr(), entry.getFloatValue()); } } private void handleModifierChange(AbilityInvokeEntry invoke) throws Exception { // Sanity checks - GameEntity target = this.player.getScene().getEntityById(invoke.getEntityId()); + var target = this.player.getScene().getEntityById(invoke.getEntityId()); if (target == null) { return; } - AbilityMetaModifierChange data = AbilityMetaModifierChange.parseFrom(invoke.getAbilityData()); + var data = AbilityMetaModifierChange.parseFrom(invoke.getAbilityData()); if (data == null) { return; } @@ -146,12 +244,32 @@ public final class AbilityManager extends BasePlayerManager { } // Sanity checks - AbilityInvokeEntryHead head = invoke.getHead(); - if (head == null) { - return; + var head = invoke.getHead(); + + if (data.getAction() == ModifierAction.MODIFIER_ACTION_REMOVED) { + var ability = target.getAbilities().get(data.getParentAbilityName().getStr()); + if(ability != null) { + var modifier = ability.getModifiers().get(head.getInstancedModifierId()); + if (modifier != null) { + modifier.onRemoved(); + ability.getModifiers().remove(modifier); + } + } } - GameEntity sourceEntity = this.player.getScene().getEntityById(data.getApplyEntityId()); + if(data.getAction() == ModifierAction.MODIFIER_ACTION_ADDED) { + var modifierString = data.getParentAbilityName().getStr(); + var hash = target.getInstanceToHash().get(head.getInstancedAbilityId()); + if(hash == null) return; + + target.getAbilities().values().stream().filter(a -> a.getHash() == hash || a.getData().abilityName == target.getInstanceToName().get(head.getInstancedAbilityId())).forEach(a -> { + a.getModifiers().keySet().stream().filter(key -> key.compareTo(modifierString) == 0).forEach(key -> { + a.getModifiers().get(key).setLocalId(head.getInstancedModifierId()); + }); + }); + } + + var sourceEntity = this.player.getScene().getEntityById(data.getApplyEntityId()); if (sourceEntity == null) { return; } @@ -243,4 +361,60 @@ public final class AbilityManager extends BasePlayerManager { default -> {} } } + + private void handleModifierDurabilityChange(AbilityInvokeEntry invoke) throws InvalidProtocolBufferException { + var target = this.player.getScene().getEntityById(invoke.getEntityId()); + if (target == null) { + return; + } + + var data = AbilityMetaModifierDurabilityChange.parseFrom(invoke.getAbilityData()); + if (data == null) { + return; + } + + var head = invoke.getHead(); + if (head == null) { + return; + } + + var hash = target.getInstanceToHash().get(head.getInstancedAbilityId()); + if(hash == null) return; + target.getAbilities().values().stream().filter(a -> a.getHash() == hash || a.getData().abilityName == target.getInstanceToName().get(head.getInstancedAbilityId())).forEach(a -> { + a.getModifiers().values().stream().filter(m -> m.getLocalId() == head.getInstancedModifierId()).forEach(modifier -> { + modifier.setElementDurability(data.getRemainDurability()); + }); + }); + } + + private void handleAddNewAbility(AbilityInvokeEntry invoke) throws InvalidProtocolBufferException { + var data = AbilityMetaAddAbility.parseFrom(invoke.getAbilityData()); + if (data == null) { + return; +} + + if(data.getAbility().getAbilityName().getHash() != 0) Grasscutter.getLogger().warn("Instancing {} in to {}", data.getAbility().getAbilityName().getHash(), data.getAbility().getInstancedAbilityId()); + else Grasscutter.getLogger().warn("Instancing {} in to {}", data.getAbility().getAbilityName().getStr(), data.getAbility().getInstancedAbilityId()); + + GameEntity target = this.player.getScene().getEntityById(invoke.getEntityId()); + if (target == null) { + return; + } + + target.getInstanceToHash().put(data.getAbility().getInstancedAbilityId(), data.getAbility().getAbilityName().getHash()); + target.getInstanceToName().put(data.getAbility().getInstancedAbilityId(), data.getAbility().getAbilityName().getStr()); + } + + public void addAbilityToEntity(GameEntity entity, String name) { + var data = GameData.getAbilityData(name); + if(data != null) + addAbilityToEntity(entity, data, name); + } + + public void addAbilityToEntity(GameEntity entity, AbilityData abilityData, String id) { + var ability = new Ability(abilityData, entity); + entity.getAbilities().put(id, ability); + + ability.onAdded(); + } } diff --git a/src/main/java/emu/grasscutter/game/ability/AbilityModifierController.java b/src/main/java/emu/grasscutter/game/ability/AbilityModifierController.java new file mode 100644 index 000000000..f1914f95f --- /dev/null +++ b/src/main/java/emu/grasscutter/game/ability/AbilityModifierController.java @@ -0,0 +1,63 @@ +package emu.grasscutter.game.ability; + +import emu.grasscutter.data.binout.AbilityModifier; +import emu.grasscutter.data.binout.AbilityModifier.AbilityModifierAction; +import emu.grasscutter.server.event.entity.EntityDamageEvent; +import lombok.Getter; +import lombok.Setter; + +public final class AbilityModifierController { + @Getter private AbilityModifier data; + + @Getter private Ability ability; //Owner ability instance + + @Getter private float elementDurability; + + @Getter @Setter private int localId; + + public AbilityModifierController(Ability ability, AbilityModifier data) { + this.ability = ability; + this.data = data; + this.elementDurability = data.elementDurability.get(); + } + + public void setElementDurability(float durability) { + this.elementDurability = durability; + + if (durability <= 0) { + onRemoved(); + ability.getModifiers().values().removeIf(a -> a == this); + } + } + + public void onAdded() { + if (data.onAdded == null) return; + + for (AbilityModifierAction action : data.onAdded) { + ability.getManager().executeAction(ability, action); + } + } + + public void onRemoved() { + if (data.onRemoved == null) return; + + for (AbilityModifierAction action : data.onRemoved) { + ability.getManager().executeAction(ability, action); + } + } + + public void onBeingHit(EntityDamageEvent event) { + if (data.onBeingHit != null) + for (var action : data.onBeingHit) { + ability.getManager().executeAction(ability, action); + } + + if (event.getAttackElementType().equals(data.elementType)) { + elementDurability -= event.getDamage(); + if(elementDurability <= 0) { + onRemoved(); + ability.getModifiers().values().removeIf(a -> a == this); + } + } + } +} diff --git a/src/main/java/emu/grasscutter/game/ability/actions/ActionApplyModifier.java b/src/main/java/emu/grasscutter/game/ability/actions/ActionApplyModifier.java new file mode 100644 index 000000000..8c6d4c60a --- /dev/null +++ b/src/main/java/emu/grasscutter/game/ability/actions/ActionApplyModifier.java @@ -0,0 +1,44 @@ +package emu.grasscutter.game.ability.actions; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.data.binout.AbilityModifier.AbilityModifierAction; +import emu.grasscutter.data.common.DynamicFloat; +import emu.grasscutter.game.ability.Ability; +import emu.grasscutter.game.ability.AbilityAction; +import emu.grasscutter.game.ability.AbilityActionHandler; +import emu.grasscutter.game.ability.AbilityModifierController; + +@AbilityAction(AbilityModifierAction.Type.ApplyModifier) +public final class ActionApplyModifier extends AbilityActionHandler { + @Override + public boolean execute(Ability ability, AbilityModifierAction action) { + var modifierData = ability.getData().modifiers.get(action.modifierName); + if (modifierData == null) { + Grasscutter.getLogger().debug("Modifier {} not found", action.modifierName); + return false; + } + + if (modifierData.stacking != null && modifierData.stacking.compareTo("Unique") == 0 && + ability.getModifiers().values().stream() + .anyMatch(m -> m.getData().equals(modifierData))) { + return true; + } + + var modifier = new AbilityModifierController(ability, modifierData); + ability.getModifiers().put(action.modifierName, modifier); + modifier.onAdded(); + + if(modifierData.duration != DynamicFloat.ZERO) { + Grasscutter.getGameServer().getScheduler().scheduleAsyncTask(() -> { + try { + Thread.sleep((int)(modifierData.duration.get() * 1000)); + modifier.onRemoved(); + } catch (InterruptedException ignored) { + Grasscutter.getLogger().error("Failed to schedule ability modifier async task."); + } + }); + } + + return true; + } +} diff --git a/src/main/java/emu/grasscutter/game/ability/actions/ActionExecuteGadgetLua.java b/src/main/java/emu/grasscutter/game/ability/actions/ActionExecuteGadgetLua.java new file mode 100644 index 000000000..ab43569bb --- /dev/null +++ b/src/main/java/emu/grasscutter/game/ability/actions/ActionExecuteGadgetLua.java @@ -0,0 +1,22 @@ +package emu.grasscutter.game.ability.actions; + +import emu.grasscutter.data.binout.AbilityModifier.AbilityModifierAction; +import emu.grasscutter.game.ability.Ability; +import emu.grasscutter.game.ability.AbilityAction; +import emu.grasscutter.game.ability.AbilityActionHandler; +import emu.grasscutter.game.entity.GameEntity; + +@AbilityAction(AbilityModifierAction.Type.ExecuteGadgetLua) +public final class ActionExecuteGadgetLua extends AbilityActionHandler { + @Override + public boolean execute(Ability ability, AbilityModifierAction action) { + var owner = ability.getOwner(); + + if( owner.getEntityController() != null) { + owner.getEntityController().onClientExecuteRequest(owner, action.param1, action.param2, action.param3); + return true; + } + + return false; + } +} diff --git a/src/main/java/emu/grasscutter/game/ability/actions/ActionKillSelf.java b/src/main/java/emu/grasscutter/game/ability/actions/ActionKillSelf.java new file mode 100644 index 000000000..010ef988b --- /dev/null +++ b/src/main/java/emu/grasscutter/game/ability/actions/ActionKillSelf.java @@ -0,0 +1,18 @@ +package emu.grasscutter.game.ability.actions; + +import emu.grasscutter.data.binout.AbilityModifier.AbilityModifierAction; +import emu.grasscutter.game.ability.Ability; +import emu.grasscutter.game.ability.AbilityAction; +import emu.grasscutter.game.ability.AbilityActionHandler; +import emu.grasscutter.game.entity.GameEntity; + +@AbilityAction(AbilityModifierAction.Type.KillSelf) +public final class ActionKillSelf extends AbilityActionHandler { + @Override + public boolean execute(Ability ability, AbilityModifierAction action) { + var owner = ability.getOwner(); + owner.getScene().killEntity(owner); + + return false; + } +} diff --git a/src/main/java/emu/grasscutter/game/activity/ActivityHandler.java b/src/main/java/emu/grasscutter/game/activity/ActivityHandler.java index c16418e6f..9d15cc7db 100644 --- a/src/main/java/emu/grasscutter/game/activity/ActivityHandler.java +++ b/src/main/java/emu/grasscutter/game/activity/ActivityHandler.java @@ -71,11 +71,11 @@ public abstract class ActivityHandler { .forEach( condGroupId -> { var condGroup = GameData.getActivityCondGroupMap().get((int) condGroupId); - condGroup - .getCondIds() - .forEach( - condition -> - questManager.queueEvent(QuestCond.QUEST_COND_ACTIVITY_COND, condition)); + if (condGroup != null) condGroup + .getCondIds() + .forEach( + condition -> + questManager.queueEvent(QuestCond.QUEST_COND_ACTIVITY_COND, condition)); }); } diff --git a/src/main/java/emu/grasscutter/game/entity/EntityGadget.java b/src/main/java/emu/grasscutter/game/entity/EntityGadget.java index 8884b0010..8976509f1 100644 --- a/src/main/java/emu/grasscutter/game/entity/EntityGadget.java +++ b/src/main/java/emu/grasscutter/game/entity/EntityGadget.java @@ -2,6 +2,7 @@ package emu.grasscutter.game.entity; import emu.grasscutter.data.GameData; import emu.grasscutter.data.binout.config.ConfigEntityGadget; +import emu.grasscutter.data.binout.config.fields.ConfigAbilityData; import emu.grasscutter.data.excels.GadgetData; import emu.grasscutter.game.entity.gadget.*; import emu.grasscutter.game.entity.gadget.platform.BaseRoute; @@ -100,8 +101,26 @@ public class EntityGadget extends EntityBaseGadget { String controllerName = GameData.getGadgetMappingMap().get(gadgetId).getServerController(); this.setEntityController(EntityControllerScriptManager.getGadgetController(controllerName)); } + + this.addConfigAbilities(); } + private void addConfigAbilities(){ + if(this.configGadget != null && this.configGadget.getAbilities() != null) { + for (var ability : this.configGadget.getAbilities()) { + this.addConfigAbility(ability); + } + } + } + + private void addConfigAbility(ConfigAbilityData abilityData){ + var data = GameData.getAbilityData(abilityData.getAbilityName()); + if(data != null) + getScene().getWorld().getHost().getAbilityManager().addAbilityToEntity( + this, data, abilityData.getAbilityID()); + } + + public void setState(int state) { this.state = state; // Cache the gadget state diff --git a/src/main/java/emu/grasscutter/game/entity/GameEntity.java b/src/main/java/emu/grasscutter/game/entity/GameEntity.java index 1ec6ed5bf..dd12c37ae 100644 --- a/src/main/java/emu/grasscutter/game/entity/GameEntity.java +++ b/src/main/java/emu/grasscutter/game/entity/GameEntity.java @@ -1,5 +1,6 @@ package emu.grasscutter.game.entity; +import emu.grasscutter.game.ability.Ability; import emu.grasscutter.game.player.Player; import emu.grasscutter.game.props.ElementType; import emu.grasscutter.game.props.FightProperty; @@ -26,6 +27,9 @@ import it.unimi.dsi.fastutil.objects.Object2FloatOpenHashMap; import lombok.Getter; import lombok.Setter; +import java.util.HashMap; +import java.util.Map; + public abstract class GameEntity { @Getter private final Scene scene; @Getter protected int id; @@ -48,6 +52,10 @@ public abstract class GameEntity { // Abilities private Object2FloatMap metaOverrideMap; private Int2ObjectMap metaModifiers; + private Map instanceToHash; + private Int2ObjectMap instanceToName; + + @Getter private Map abilities = new HashMap<>(); public GameEntity(Scene scene) { this.scene = scene; @@ -86,6 +94,22 @@ public abstract class GameEntity { return this.metaModifiers; } + public Map getInstanceToHash() { + if (this.instanceToHash == null) { + this.instanceToHash = new HashMap<>(); + } + + return this.instanceToHash; + } + + public Int2ObjectMap getInstanceToName() { + if (this.instanceToName == null) { + this.instanceToName = new Int2ObjectOpenHashMap<>(); + } + + return this.instanceToName; + } + public abstract Int2FloatMap getFightProperties(); public abstract Position getPosition(); @@ -189,7 +213,9 @@ public abstract class GameEntity { this.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, 0f); isDead = true; } + this.runLuaCallbacks(event); + this.runAbilityCallbacks(event); // Packets this.getScene() @@ -213,6 +239,15 @@ public abstract class GameEntity { } } + /** + * Runs the ability callbacks for {@link EntityDamageEvent}. + * + * @param event The damage event. + */ + public void runAbilityCallbacks(EntityDamageEvent event) { + this.abilities.values().forEach(ability -> ability.onBeingHit(event)); + } + /** * Move this entity to a new position. * diff --git a/src/main/java/emu/grasscutter/game/player/Player.java b/src/main/java/emu/grasscutter/game/player/Player.java index e7c469541..040f7a7be 100644 --- a/src/main/java/emu/grasscutter/game/player/Player.java +++ b/src/main/java/emu/grasscutter/game/player/Player.java @@ -169,6 +169,8 @@ public class Player { @Getter private transient PlayerProgressManager progressManager; @Getter private transient SatiationManager satiationManager; + @Getter @Setter private transient Position lastCheckedPosition = null; + // Manager data (Save-able to the database) @Getter private transient Achievements achievements; private PlayerProfile playerProfile; // Getter has null-check diff --git a/src/main/java/emu/grasscutter/game/world/Scene.java b/src/main/java/emu/grasscutter/game/world/Scene.java index 10594cb03..82e5e918c 100644 --- a/src/main/java/emu/grasscutter/game/world/Scene.java +++ b/src/main/java/emu/grasscutter/game/world/Scene.java @@ -330,16 +330,30 @@ public final class Scene { addEntities(entities, VisionType.VISION_TYPE_BORN); } + private static List> chopped(List list, final int L) { + List> parts = new ArrayList>(); + final int N = list.size(); + for (int i = 0; i < N; i += L) { + parts.add(new ArrayList( + list.subList(i, Math.min(N, i + L))) + ); + } + return parts; + } + public synchronized void addEntities( Collection entities, VisionType visionType) { if (entities == null || entities.isEmpty()) { return; } - for (GameEntity entity : entities) { + + for (var entity : entities) { this.addEntityDirectly(entity); } - this.broadcastPacket(new PacketSceneEntityAppearNotify(entities, visionType)); + for(var l : chopped(new ArrayList<>(entities), 100)) { + this.broadcastPacket(new PacketSceneEntityAppearNotify(l, visionType)); + } } private GameEntity removeEntityDirectly(GameEntity entity) { diff --git a/src/main/java/emu/grasscutter/scripts/SceneScriptManager.java b/src/main/java/emu/grasscutter/scripts/SceneScriptManager.java index c0668264c..fb8584deb 100644 --- a/src/main/java/emu/grasscutter/scripts/SceneScriptManager.java +++ b/src/main/java/emu/grasscutter/scripts/SceneScriptManager.java @@ -28,12 +28,16 @@ import emu.grasscutter.utils.Position; import io.netty.util.concurrent.FastThreadLocalThread; import java.io.FileWriter; import java.io.IOException; +import java.nio.file.Files; import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import javax.annotation.Nonnull; import javax.annotation.Nullable; + +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import kotlin.Pair; import lombok.val; import org.luaj.vm2.LuaError; @@ -45,6 +49,7 @@ public class SceneScriptManager { private final Map variables; private SceneMeta meta; private boolean isInit; + /** current triggers controlled by RefreshGroup */ private final Map> currentTriggers; @@ -59,8 +64,8 @@ public class SceneScriptManager { private ScriptMonsterSpawnService scriptMonsterSpawnService; /** blockid - loaded groupSet */ private final Map> loadedGroupSetPerBlock; - - private List groupGrids; + private static final Int2ObjectMap> groupGridsCache + = new Int2ObjectOpenHashMap<>(); public static final ExecutorService eventExecutor; static { @@ -89,7 +94,6 @@ public class SceneScriptManager { this.cachedSceneGroupsInstances = new ConcurrentHashMap<>(); this.scriptMonsterSpawnService = new ScriptMonsterSpawnService(this); this.loadedGroupSetPerBlock = new ConcurrentHashMap<>(); - this.groupGrids = null; // This is changed on init // TEMPORARY if (this.getScene().getId() < 10 @@ -105,10 +109,6 @@ public class SceneScriptManager { return scene; } - public List getGroupGrids() { - return groupGrids; - } - public SceneConfig getConfig() { return this.isInit ? this.meta.config : null; } @@ -118,7 +118,8 @@ public class SceneScriptManager { } @Nullable public Map getVariables(int group_id) { - if (getCachedGroupInstanceById(group_id) == null) return null; + if (this.getCachedGroupInstanceById(group_id) == null) + return Collections.emptyMap(); return getCachedGroupInstanceById(group_id).getCachedVariables(); } @@ -199,8 +200,11 @@ public class SceneScriptManager { // } } - public int refreshGroup( - SceneGroupInstance groupInstance, int suiteIndex, boolean excludePrevSuite) { + public int refreshGroup(SceneGroupInstance groupInstance, int suiteIndex, boolean excludePrevSuite) { + return this.refreshGroup(groupInstance, suiteIndex, excludePrevSuite, null); + } + + public int refreshGroup(SceneGroupInstance groupInstance, int suiteIndex, boolean excludePrevSuite, List entitiesAdded) { SceneGroup group = groupInstance.getLuaGroup(); if (suiteIndex == 0) { if (excludePrevSuite) { @@ -242,7 +246,7 @@ public class SceneScriptManager { removeGroupSuite(group, prevSuiteData); } // Remove old group suite - addGroupSuite(groupInstance, suiteData); + this.addGroupSuite(groupInstance, suiteData, entitiesAdded); // Refesh variables here group.variables.forEach( @@ -423,152 +427,104 @@ public class SceneScriptManager { } this.meta = meta; - var path = FileUtils.getScriptPath("Scene/" + getScene().getId() + "/scene_grid.json"); + // TEMP + this.isInit = true; + } - try { - this.groupGrids = JsonUtils.loadToList(path, Grid.class); - this.groupGrids.forEach(Grid::load); - } catch (IOException ignored) { - Grasscutter.getLogger().error("Scene {} unable to load grid file.", getScene().getId()); - } catch (Exception e) { - Grasscutter.getLogger().error("Scene {} unable to load grid file.", getScene().getId(), e); - } + public List getGroupGrids() { + int sceneId = scene.getId(); + if (groupGridsCache.containsKey(sceneId) && groupGridsCache.get(sceneId) != null) { + Grasscutter.getLogger().debug("Hit cache for scene {}",sceneId); + return groupGridsCache.get(sceneId); + } else { + var path = FileUtils.getCachePath("scene" + sceneId + "_grid.json"); + if (path.toFile().isFile() && !Grasscutter.config.server.game.cacheSceneEntitiesEveryRun) { + try { + var groupGrids = JsonUtils.loadToList(path, Grid.class); + groupGridsCache.put(sceneId, groupGrids); + if(groupGrids != null) return groupGrids; + } catch (IOException e) { + e.printStackTrace(); + } + } - boolean runForFirstTime = this.groupGrids == null; - - // Find if the scene entities are already generated, if not generate it - if (Grasscutter.getConfig().server.game.cacheSceneEntitiesEveryRun || runForFirstTime) { + //otherwise generate the grids List>> groupPositions = new ArrayList<>(); for (int i = 0; i < 6; i++) groupPositions.add(new HashMap<>()); - var visionOptions = Grasscutter.getConfig().server.game.visionOptions; - meta.blocks - .values() - .forEach( - block -> { - block.load(scene.getId(), meta.context); - block.groups.values().stream() - .filter(g -> !g.dynamic_load) - .forEach( - group -> { - group.load(this.scene.getId()); + var visionOptions = Grasscutter.config.server.game.visionOptions; + meta.blocks.values().forEach(block -> { + block.load(sceneId, meta.context); + block.groups.values().stream().filter(g -> !g.dynamic_load).forEach(group -> { + group.load(this.scene.getId()); - // Add all entitites here - Set vision_levels = new HashSet<>(); + //Add all entities here + Set vision_levels = new HashSet<>(); - if (group.monsters != null) { - group - .monsters - .values() - .forEach( - m -> { - addGridPositionToMap( - groupPositions.get(m.vision_level), - group.id, - m.vision_level, - m.pos); - vision_levels.add(m.vision_level); - }); - } else { - Grasscutter.getLogger() - .error("group.monsters null for group {}", group.id); - } - if (group.gadgets != null) { - group - .gadgets - .values() - .forEach( - g -> { - int vision_level = - Math.max( - getGadgetVisionLevel(g.gadget_id), g.vision_level); - addGridPositionToMap( - groupPositions.get(vision_level), - group.id, - vision_level, - g.pos); - vision_levels.add(vision_level); - }); - } else { - Grasscutter.getLogger() - .error("group.gadgets null for group {}", group.id); - } + if (group.monsters != null) { + group.monsters.values().forEach(m -> { + addGridPositionToMap(groupPositions.get(m.vision_level), group.id, m.vision_level, m.pos); + vision_levels.add(m.vision_level); + }); + } else { + Grasscutter.getLogger().error("group.monsters null for group {}", group.id); + } + if (group.gadgets != null) { + group.gadgets.values().forEach(g -> { + int vision_level = Math.max(getGadgetVisionLevel(g.gadget_id), g.vision_level); + addGridPositionToMap(groupPositions.get(vision_level), group.id, vision_level, g.pos); + vision_levels.add(vision_level); + }); + } else { + Grasscutter.getLogger().error("group.gadgets null for group {}", group.id); + } - if (group.npcs != null) { - group - .npcs - .values() - .forEach( - n -> - addGridPositionToMap( - groupPositions.get(n.vision_level), - group.id, - n.vision_level, - n.pos)); - } else { - Grasscutter.getLogger().error("group.npcs null for group {}", group.id); - } + if (group.npcs != null) { + group.npcs.values().forEach(n -> addGridPositionToMap(groupPositions.get(n.vision_level), group.id, n.vision_level, n.pos)); + } else { + Grasscutter.getLogger().error("group.npcs null for group {}", group.id); + } - if (group.regions != null) { - group - .regions - .values() - .forEach( - r -> - addGridPositionToMap( - groupPositions.get(0), group.id, 0, r.pos)); - } else { - Grasscutter.getLogger() - .error("group.regions null for group {}", group.id); - } + if (group.regions != null) { + group.regions.values().forEach(r -> addGridPositionToMap(groupPositions.get(0), group.id, 0, r.pos)); + } else { + Grasscutter.getLogger().error("group.regions null for group {}", group.id); + } - if (group.garbages != null && group.garbages.gadgets != null) - group.garbages.gadgets.forEach( - g -> - addGridPositionToMap( - groupPositions.get(g.vision_level), - group.id, - g.vision_level, - g.pos)); + if (group.garbages != null && group.garbages.gadgets != null) + group.garbages.gadgets.forEach(g -> addGridPositionToMap(groupPositions.get(g.vision_level), group.id, g.vision_level, g.pos)); - int max_vision_level = -1; - if (!vision_levels.isEmpty()) { - for (int vision_level : vision_levels) { - if (max_vision_level == -1 - || visionOptions[max_vision_level].visionRange - < visionOptions[vision_level].visionRange) - max_vision_level = vision_level; - } - } - if (max_vision_level == -1) max_vision_level = 0; + int max_vision_level = -1; + if (!vision_levels.isEmpty()) { + for (int vision_level : vision_levels) { + if (max_vision_level == -1 || visionOptions[max_vision_level].visionRange < visionOptions[vision_level].visionRange) + max_vision_level = vision_level; + } + } + if (max_vision_level == -1) max_vision_level = 0; - addGridPositionToMap( - groupPositions.get(max_vision_level), - group.id, - max_vision_level, - group.pos); - }); - }); + addGridPositionToMap(groupPositions.get(max_vision_level), group.id, max_vision_level, group.pos); + }); + }); - this.groupGrids = new ArrayList<>(); + var groupGrids = new ArrayList(); for (int i = 0; i < 6; i++) { - this.groupGrids.add(new Grid()); - this.groupGrids.get(i).gridMap = groupPositions.get(i); + groupGrids.add(new Grid()); + groupGrids.get(i).grid = groupPositions.get(i); } + groupGridsCache.put(scene.getId(), groupGrids); - try (FileWriter file = new FileWriter(path.toFile())) { + try { + Files.createDirectories(path.getParent()); + } catch (IOException ignored) {} + try (var file = new FileWriter(path.toFile())) { file.write(JsonUtils.encode(groupGrids)); - } catch (IOException ignored) { - Grasscutter.getLogger().error("Scene {} unable to write to grid file.", getScene().getId()); + Grasscutter.getLogger().info("Scene {} saved grid file.", getScene().getId()); } catch (Exception e) { - Grasscutter.getLogger().error("Scene {} unable to save grid file.", e, getScene().getId()); + Grasscutter.getLogger().error("Scene {} unable to save grid file.", getScene().getId(), e); } - - Grasscutter.getLogger().info("Scene {} saved grid file.", getScene().getId()); + return groupGrids; } - - // TEMP - this.isInit = true; } public boolean isInit() { @@ -699,6 +655,10 @@ public class SceneScriptManager { } public void addGroupSuite(SceneGroupInstance groupInstance, SceneSuite suite) { + this.addGroupSuite(groupInstance, suite, null); + } + + public void addGroupSuite(SceneGroupInstance groupInstance, SceneSuite suite, List entities) { // we added trigger first registerTrigger(suite.sceneTriggers); @@ -706,7 +666,9 @@ public class SceneScriptManager { var toCreate = new ArrayList(); toCreate.addAll(getGadgetsInGroupSuite(groupInstance, suite)); toCreate.addAll(getMonstersInGroupSuite(groupInstance, suite)); - addEntities(toCreate); + if (entities != null) + entities.addAll(toCreate); + else this.addEntities(toCreate); registerRegionInGroupSuite(group, suite); } diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerClientAbilityChangeNotify.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerClientAbilityChangeNotify.java new file mode 100644 index 000000000..0dc9185b3 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerClientAbilityChangeNotify.java @@ -0,0 +1,21 @@ +package emu.grasscutter.server.packet.recv; + +import emu.grasscutter.net.packet.Opcodes; +import emu.grasscutter.net.packet.PacketHandler; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.ClientAbilityChangeNotifyOuterClass.ClientAbilityChangeNotify; +import emu.grasscutter.server.game.GameSession; + +@Opcodes(PacketOpcodes.ClientAbilityChangeNotify) +public final class HandlerClientAbilityChangeNotify extends PacketHandler { + @Override + public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { + var notif = ClientAbilityChangeNotify.parseFrom(payload); + + var player = session.getPlayer(); + for (var entry : notif.getInvokesList()) { + player.getAbilityManager().onAbilityInvoke(entry); + player.getAbilityInvokeHandler().addEntry(entry.getForwardType(), entry); + } + } +} diff --git a/src/main/java/emu/grasscutter/utils/GridPosition.java b/src/main/java/emu/grasscutter/utils/GridPosition.java index 3f7996b9e..654195afa 100644 --- a/src/main/java/emu/grasscutter/utils/GridPosition.java +++ b/src/main/java/emu/grasscutter/utils/GridPosition.java @@ -1,5 +1,6 @@ package emu.grasscutter.utils; +import com.github.davidmoten.rtreemulti.geometry.Point; import dev.morphia.annotations.Entity; import java.io.IOException; import java.io.Serializable; @@ -9,7 +10,7 @@ import lombok.Setter; import lombok.SneakyThrows; @Entity -public class GridPosition implements Serializable { +public final class GridPosition implements Serializable { private static final long serialVersionUID = -2001232300615923575L; @Getter @Setter private int x; @@ -38,8 +39,7 @@ public class GridPosition implements Serializable { this.x = xzwidth.get(0); } - @SneakyThrows - public GridPosition(String str) { + public GridPosition(String str) throws IOException { String[] listOfParams = str.replace(" ", "").replace("(", "").replace(")", "").split(","); if (listOfParams.length != 3) throw new IOException("invalid size on GridPosition definition - "); @@ -91,15 +91,23 @@ public class GridPosition implements Serializable { return new int[] {x, z, width}; } + public double[] toDoubleArray() { + return new double[]{ x, z }; + } + public int[] toXZIntArray() { return new int[] {x, z}; } + public Point toPoint() { + return Point.create(x,z); + } + @Override public int hashCode() { - int result = (int) (x ^ (x >>> 32)); - result = 31 * result + (int) (z ^ (z >>> 32)); - result = 31 * result + (int) (width ^ (width >>> 32)); + int result = x ^ (x >>> 32); + result = 31 * result + (z ^ (z >>> 32)); + result = 31 * result + (width ^ (width >>> 32)); return result; }