From 7c35c51a6afd29bfa0731b308f9100e80182a9c6 Mon Sep 17 00:00:00 2001
From: HotaruYS <105128850+HotaruYS@users.noreply.github.com>
Date: Sun, 8 May 2022 12:48:06 +0200
Subject: [PATCH 1/4] Override server logging level with environment variable
(#653)
Use `LOG_LEVEL` environment variable to override logging level for `emu.grasscutter` (which also contains all loggers under it). This might help with debugging various issues reported by users. Previously, the only way to override these levels would be to use `-Dlogback.configurationFile`
---
src/main/resources/logback.xml | 9 ++++++---
1 file changed, 6 insertions(+), 3 deletions(-)
diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml
index 1fc6831cb..bd0740fca 100644
--- a/src/main/resources/logback.xml
+++ b/src/main/resources/logback.xml
@@ -1,4 +1,6 @@
+
+
[%d{HH:mm:ss}] [%highlight(%level)] %msg%n
@@ -14,11 +16,12 @@
%d{yyyy-MM-dd'T'HH:mm:ss'Z'} - %m%n
-
+
+
+
+
-
-
\ No newline at end of file
From a09723f07d15618585036d180c1c11b72d1150a6 Mon Sep 17 00:00:00 2001
From: gentlespoon
Date: Sun, 8 May 2022 01:09:53 -0700
Subject: [PATCH 2/4] Fix: timer is already cancelled.
---
.../managers/StaminaManager/StaminaManager.java | 16 +++++++---------
1 file changed, 7 insertions(+), 9 deletions(-)
diff --git a/src/main/java/emu/grasscutter/game/managers/StaminaManager/StaminaManager.java b/src/main/java/emu/grasscutter/game/managers/StaminaManager/StaminaManager.java
index 5065b12b3..5947880e7 100644
--- a/src/main/java/emu/grasscutter/game/managers/StaminaManager/StaminaManager.java
+++ b/src/main/java/emu/grasscutter/game/managers/StaminaManager/StaminaManager.java
@@ -29,9 +29,7 @@ public class StaminaManager {
private Position previousCoordinates = new Position(0, 0, 0);
private MotionState currentState = MotionState.MOTION_STANDBY;
private MotionState previousState = MotionState.MOTION_STANDBY;
- private final Timer sustainedStaminaHandlerTimer = new Timer();
- private final SustainedStaminaHandler handleSustainedStamina = new SustainedStaminaHandler();
- private boolean timerRunning = false;
+ private Timer sustainedStaminaHandlerTimer;
private GameSession cachedSession = null;
private GameEntity cachedEntity = null;
private int staminaRecoverDelay = 0;
@@ -136,21 +134,21 @@ public class StaminaManager {
entity.getWorld().broadcastPacket(new PacketEntityFightPropUpdateNotify(entity, FightProperty.FIGHT_PROP_CUR_HP));
entity.getWorld().broadcastPacket(new PacketLifeStateChangeNotify(0, entity, LifeState.LIFE_DEAD));
player.getScene().removeEntity(entity);
- ((EntityAvatar)entity).onDeath(dieType, 0);
+ ((EntityAvatar) entity).onDeath(dieType, 0);
}
public void startSustainedStaminaHandler() {
- if (!player.isPaused() && !timerRunning) {
- timerRunning = true;
- sustainedStaminaHandlerTimer.scheduleAtFixedRate(handleSustainedStamina, 0, 200);
+ if (!player.isPaused() && sustainedStaminaHandlerTimer == null) {
+ sustainedStaminaHandlerTimer = new Timer();
+ sustainedStaminaHandlerTimer.scheduleAtFixedRate(new SustainedStaminaHandler(), 0, 200);
// Grasscutter.getLogger().debug("[MovementManager] SustainedStaminaHandlerTimer started");
}
}
public void stopSustainedStaminaHandler() {
- if (timerRunning) {
- timerRunning = false;
+ if (sustainedStaminaHandlerTimer != null) {
sustainedStaminaHandlerTimer.cancel();
+ sustainedStaminaHandlerTimer = null;
// Grasscutter.getLogger().debug("[MovementManager] SustainedStaminaHandlerTimer stopped");
}
}
From d78348522e93ac694ef89d9885b3e1c739b29fb7 Mon Sep 17 00:00:00 2001
From: gentlespoon
Date: Sun, 8 May 2022 04:02:45 -0700
Subject: [PATCH 3/4] Update StaminaManager
---
.../AfterUpdateStaminaListener.java | 12 +
.../BeforeUpdateStaminaListener.java | 20 ++
.../StaminaManager/ConsumptionType.java | 8 +-
.../game/managers/StaminaManager/README.md | 73 +++++
.../StaminaManager/StaminaManager.java | 265 +++++++++++++-----
.../recv/HandlerCombatInvocationsNotify.java | 44 +--
6 files changed, 327 insertions(+), 95 deletions(-)
create mode 100644 src/main/java/emu/grasscutter/game/managers/StaminaManager/AfterUpdateStaminaListener.java
create mode 100644 src/main/java/emu/grasscutter/game/managers/StaminaManager/BeforeUpdateStaminaListener.java
create mode 100644 src/main/java/emu/grasscutter/game/managers/StaminaManager/README.md
diff --git a/src/main/java/emu/grasscutter/game/managers/StaminaManager/AfterUpdateStaminaListener.java b/src/main/java/emu/grasscutter/game/managers/StaminaManager/AfterUpdateStaminaListener.java
new file mode 100644
index 000000000..bb4f0b188
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/managers/StaminaManager/AfterUpdateStaminaListener.java
@@ -0,0 +1,12 @@
+package emu.grasscutter.game.managers.StaminaManager;
+
+public interface AfterUpdateStaminaListener {
+ /**
+ * onBeforeUpdateStamina() will be called before StaminaManager attempt to update the player's current stamina.
+ * This gives listeners a chance to intercept this update.
+ *
+ * @param reason Why updating stamina.
+ * @param newStamina New Stamina value.
+ */
+ void onAfterUpdateStamina(String reason, int newStamina);
+}
diff --git a/src/main/java/emu/grasscutter/game/managers/StaminaManager/BeforeUpdateStaminaListener.java b/src/main/java/emu/grasscutter/game/managers/StaminaManager/BeforeUpdateStaminaListener.java
new file mode 100644
index 000000000..02f1f3522
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/managers/StaminaManager/BeforeUpdateStaminaListener.java
@@ -0,0 +1,20 @@
+package emu.grasscutter.game.managers.StaminaManager;
+
+public interface BeforeUpdateStaminaListener {
+ /**
+ * onBeforeUpdateStamina() will be called before StaminaManager attempt to update the player's current stamina.
+ * This gives listeners a chance to intercept this update.
+ * @param reason Why updating stamina.
+ * @param newStamina New ABSOLUTE stamina value.
+ * @return true if you want to cancel this update, otherwise false.
+ */
+ int onBeforeUpdateStamina(String reason, int newStamina);
+ /**
+ * onBeforeUpdateStamina() will be called before StaminaManager attempt to update the player's current stamina.
+ * This gives listeners a chance to intercept this update.
+ * @param reason Why updating stamina.
+ * @param consumption ConsumptionType and RELATIVE stamina change amount.
+ * @return true if you want to cancel this update, otherwise false.
+ */
+ Consumption onBeforeUpdateStamina(String reason, Consumption consumption);
+}
\ No newline at end of file
diff --git a/src/main/java/emu/grasscutter/game/managers/StaminaManager/ConsumptionType.java b/src/main/java/emu/grasscutter/game/managers/StaminaManager/ConsumptionType.java
index 9a2d8ae24..9afb2171c 100644
--- a/src/main/java/emu/grasscutter/game/managers/StaminaManager/ConsumptionType.java
+++ b/src/main/java/emu/grasscutter/game/managers/StaminaManager/ConsumptionType.java
@@ -10,10 +10,10 @@ public enum ConsumptionType {
SPRINT(-1800),
DASH(-360),
FLY(-60),
- SWIM_DASH_START(-200),
- SWIM_DASH(-200),
- SWIMMING(-80),
- FIGHT(0),
+ SWIM_DASH_START(-20),
+ SWIM_DASH(-204),
+ SWIMMING(-80), // TODO: Slow swimming is handled per movement, not per second. Movement frequency depends on gender/age/height.
+ FIGHT(0), // See StaminaManager.getFightConsumption()
// restore
STANDBY(500),
diff --git a/src/main/java/emu/grasscutter/game/managers/StaminaManager/README.md b/src/main/java/emu/grasscutter/game/managers/StaminaManager/README.md
new file mode 100644
index 000000000..39a4e7988
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/managers/StaminaManager/README.md
@@ -0,0 +1,73 @@
+# Stamina Manager
+
+---
+## UpdateStamina
+```java
+// will use consumption.consumptionType as reason
+public int updateStaminaRelative(GameSession session, Consumption consumption);
+```
+```java
+public int updateStaminaAbsolute(GameSession session, String reason, int newStamina)
+```
+
+---
+## Pause and Resume
+```java
+public void startSustainedStaminaHandler()
+```
+```java
+public void stopSustainedStaminaHandler()
+```
+
+
+---
+## Stamina change listeners and intercepting
+### BeforeUpdateStaminaListener
+```java
+
+import emu.grasscutter.game.managers.StaminaManager.BeforeUpdateStaminaListener;
+
+// Listener sample: plugin disable CLIMB_JUMP stamina cost.
+private class MyClass implements BeforeUpdateStaminaListener {
+ // Make your class implement the listener, and pass in your class as a listener.
+
+ public MyClass() {
+ getStaminaManager().registerBeforeUpdateStaminaListener("myClass", this);
+ }
+
+ @Override
+ public boolean onBeforeUpdateStamina(String reason, int newStamina) {
+ // do not intercept this update
+ return false;
+ }
+
+ @Override
+ public boolean onBeforeUpdateStamina(String reason, Consumption consumption) {
+ // Try to intercept if this update is CLIMB_JUMP
+ if (consumption.consumptionType == ConsumptionType.CLIMB_JUMP) {
+ return true;
+ }
+ // If it is not CLIMB_JUMP, do not intercept.
+ return false;
+ }
+}
+```
+### AfterUpdateStaminaListener
+```java
+
+import emu.grasscutter.game.managers.StaminaManager.AfterUpdateStaminaListener;
+
+// Listener sample: plugin listens for changes already made.
+private class MyClass implements AfterUpdateStaminaListener {
+ // Make your class implement the listener, and pass in your class as a listener.
+
+ public MyClass() {
+ registerAfterUpdateStaminaListener("myClass", this);
+ }
+
+ @Override
+ public void onAfterUpdateStamina(String reason, int newStamina) {
+ // ...
+ }
+}
+```
\ No newline at end of file
diff --git a/src/main/java/emu/grasscutter/game/managers/StaminaManager/StaminaManager.java b/src/main/java/emu/grasscutter/game/managers/StaminaManager/StaminaManager.java
index 5947880e7..72b91c055 100644
--- a/src/main/java/emu/grasscutter/game/managers/StaminaManager/StaminaManager.java
+++ b/src/main/java/emu/grasscutter/game/managers/StaminaManager/StaminaManager.java
@@ -33,94 +33,170 @@ public class StaminaManager {
private GameSession cachedSession = null;
private GameEntity cachedEntity = null;
private int staminaRecoverDelay = 0;
- private boolean isInSkillMove = false;
- public boolean getIsInSkillMove() {
- return isInSkillMove;
- }
- public void setIsInSkillMove(boolean b) {
- isInSkillMove = b;
- }
+
+ private HashMap beforeUpdateStaminaListeners = new HashMap<>();
+ private HashMap afterUpdateStaminaListeners = new HashMap<>();
public StaminaManager(Player player) {
this.player = player;
MotionStatesCategorized.put("SWIM", new HashSet<>(Arrays.asList(
- MotionState.MOTION_SWIM_MOVE,
- MotionState.MOTION_SWIM_IDLE,
- MotionState.MOTION_SWIM_DASH,
- MotionState.MOTION_SWIM_JUMP
+ MotionState.MOTION_SWIM_MOVE,
+ MotionState.MOTION_SWIM_IDLE,
+ MotionState.MOTION_SWIM_DASH,
+ MotionState.MOTION_SWIM_JUMP
)));
MotionStatesCategorized.put("STANDBY", new HashSet<>(Arrays.asList(
- MotionState.MOTION_STANDBY,
- MotionState.MOTION_STANDBY_MOVE,
- MotionState.MOTION_DANGER_STANDBY,
- MotionState.MOTION_DANGER_STANDBY_MOVE,
- MotionState.MOTION_LADDER_TO_STANDBY,
- MotionState.MOTION_JUMP_UP_WALL_FOR_STANDBY
+ MotionState.MOTION_STANDBY,
+ MotionState.MOTION_STANDBY_MOVE,
+ MotionState.MOTION_DANGER_STANDBY,
+ MotionState.MOTION_DANGER_STANDBY_MOVE,
+ MotionState.MOTION_LADDER_TO_STANDBY,
+ MotionState.MOTION_JUMP_UP_WALL_FOR_STANDBY
)));
MotionStatesCategorized.put("CLIMB", new HashSet<>(Arrays.asList(
- MotionState.MOTION_CLIMB,
- MotionState.MOTION_CLIMB_JUMP,
- MotionState.MOTION_STANDBY_TO_CLIMB,
- MotionState.MOTION_LADDER_IDLE,
- MotionState.MOTION_LADDER_MOVE,
- MotionState.MOTION_LADDER_SLIP,
- MotionState.MOTION_STANDBY_TO_LADDER
+ MotionState.MOTION_CLIMB,
+ MotionState.MOTION_CLIMB_JUMP,
+ MotionState.MOTION_STANDBY_TO_CLIMB,
+ MotionState.MOTION_LADDER_IDLE,
+ MotionState.MOTION_LADDER_MOVE,
+ MotionState.MOTION_LADDER_SLIP,
+ MotionState.MOTION_STANDBY_TO_LADDER
)));
MotionStatesCategorized.put("FLY", new HashSet<>(Arrays.asList(
- MotionState.MOTION_FLY,
- MotionState.MOTION_FLY_IDLE,
- MotionState.MOTION_FLY_SLOW,
- MotionState.MOTION_FLY_FAST,
- MotionState.MOTION_POWERED_FLY
+ MotionState.MOTION_FLY,
+ MotionState.MOTION_FLY_IDLE,
+ MotionState.MOTION_FLY_SLOW,
+ MotionState.MOTION_FLY_FAST,
+ MotionState.MOTION_POWERED_FLY
)));
MotionStatesCategorized.put("RUN", new HashSet<>(Arrays.asList(
- MotionState.MOTION_DASH,
- MotionState.MOTION_DANGER_DASH,
- MotionState.MOTION_DASH_BEFORE_SHAKE,
- MotionState.MOTION_RUN,
- MotionState.MOTION_DANGER_RUN,
- MotionState.MOTION_WALK,
- MotionState.MOTION_DANGER_WALK
+ MotionState.MOTION_DASH,
+ MotionState.MOTION_DANGER_DASH,
+ MotionState.MOTION_DASH_BEFORE_SHAKE,
+ MotionState.MOTION_RUN,
+ MotionState.MOTION_DANGER_RUN,
+ MotionState.MOTION_WALK,
+ MotionState.MOTION_DANGER_WALK
)));
MotionStatesCategorized.put("FIGHT", new HashSet<>(Arrays.asList(
- MotionState.MOTION_FIGHT
+ MotionState.MOTION_FIGHT
)));
+
+ MotionStatesCategorized.put("SKIFF", new HashSet<>(Arrays.asList(
+ MotionState.MOTION_SKIFF_BOARDING,
+ MotionState.MOTION_SKIFF_NORMAL,
+ MotionState.MOTION_SKIFF_DASH,
+ MotionState.MOTION_SKIFF_POWERED_DASH
+ )));
+ }
+
+ // Listeners
+
+ public boolean registerBeforeUpdateStaminaListener(String listenerName, BeforeUpdateStaminaListener listener) {
+ if (beforeUpdateStaminaListeners.containsKey(listenerName)) {
+ return false;
+ }
+ beforeUpdateStaminaListeners.put(listenerName, listener);
+ return true;
+ }
+
+ public boolean unregisterBeforeUpdateStaminaListener(String listenerName) {
+ if (!beforeUpdateStaminaListeners.containsKey(listenerName)) {
+ return false;
+ }
+ beforeUpdateStaminaListeners.remove(listenerName);
+ return true;
+ }
+
+ public boolean registerAfterUpdateStaminaListener(String listenerName, AfterUpdateStaminaListener listener) {
+ if (afterUpdateStaminaListeners.containsKey(listenerName)) {
+ return false;
+ }
+ afterUpdateStaminaListeners.put(listenerName, listener);
+ return true;
+ }
+
+ public boolean unregisterAfterUpdateStaminaListener(String listenerName) {
+ if (!afterUpdateStaminaListeners.containsKey(listenerName)) {
+ return false;
+ }
+ afterUpdateStaminaListeners.remove(listenerName);
+ return true;
}
private boolean isPlayerMoving() {
float diffX = currentCoordinates.getX() - previousCoordinates.getX();
float diffY = currentCoordinates.getY() - previousCoordinates.getY();
float diffZ = currentCoordinates.getZ() - previousCoordinates.getZ();
- Grasscutter.getLogger().debug("isPlayerMoving: " + previousCoordinates + ", " + currentCoordinates +
+ Grasscutter.getLogger().trace("isPlayerMoving: " + previousCoordinates + ", " + currentCoordinates +
", " + diffX + ", " + diffY + ", " + diffZ);
return Math.abs(diffX) > 0.3 || Math.abs(diffY) > 0.2 || Math.abs(diffZ) > 0.3;
}
- // Returns new stamina and sends PlayerPropNotify
- public int updateStamina(GameSession session, Consumption consumption) {
+ public int updateStaminaRelative(GameSession session, Consumption consumption) {
int currentStamina = player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA);
if (consumption.amount == 0) {
return currentStamina;
}
+ // notify will update
+ for (Map.Entry listener : beforeUpdateStaminaListeners.entrySet()) {
+ Consumption overriddenConsumption = listener.getValue().onBeforeUpdateStamina(consumption.consumptionType.toString(), consumption);
+ if ((overriddenConsumption.consumptionType != consumption.consumptionType) && (overriddenConsumption.amount != consumption.amount)) {
+ Grasscutter.getLogger().debug("[StaminaManager] Stamina update relative(" +
+ consumption.consumptionType.toString() + ", " + consumption.amount + ") overridden to relative(" +
+ consumption.consumptionType.toString() + ", " + consumption.amount + ") by: " + listener.getKey());
+ return currentStamina;
+ }
+ }
int playerMaxStamina = player.getProperty(PlayerProperty.PROP_MAX_STAMINA);
- Grasscutter.getLogger().debug(currentStamina + "/" + playerMaxStamina + "\t" + currentState + "\t" +
+ Grasscutter.getLogger().trace(currentStamina + "/" + playerMaxStamina + "\t" + currentState + "\t" +
(isPlayerMoving() ? "moving" : " ") + "\t(" + consumption.consumptionType + "," +
consumption.amount + ")");
int newStamina = currentStamina + consumption.amount;
if (newStamina < 0) {
newStamina = 0;
- }
- if (newStamina > playerMaxStamina) {
+ } else if (newStamina > playerMaxStamina) {
newStamina = playerMaxStamina;
}
+ return setStamina(session, consumption.consumptionType.toString(), newStamina);
+ }
+
+ public int updateStaminaAbsolute(GameSession session, String reason, int newStamina) {
+ int currentStamina = player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA);
+ // notify will update
+ for (Map.Entry listener : beforeUpdateStaminaListeners.entrySet()) {
+ int overriddenNewStamina = listener.getValue().onBeforeUpdateStamina(reason, newStamina);
+ if (overriddenNewStamina != newStamina) {
+ Grasscutter.getLogger().debug("[StaminaManager] Stamina update absolute(" +
+ reason + ", " + newStamina + ") overridden to absolute(" +
+ reason + ", " + newStamina + ") by: " + listener.getKey());
+ return currentStamina;
+ }
+ }
+ int playerMaxStamina = player.getProperty(PlayerProperty.PROP_MAX_STAMINA);
+ if (newStamina < 0) {
+ newStamina = 0;
+ } else if (newStamina > playerMaxStamina) {
+ newStamina = playerMaxStamina;
+ }
+ return setStamina(session, reason, newStamina);
+ }
+
+ // Returns new stamina and sends PlayerPropNotify
+ public int setStamina(GameSession session, String reason, int newStamina) {
+ // set stamina
player.setProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA, newStamina);
session.send(new PacketPlayerPropNotify(player, PlayerProperty.PROP_CUR_PERSIST_STAMINA));
+ // notify updated
+ for (Map.Entry listener : afterUpdateStaminaListeners.entrySet()) {
+ listener.getValue().onAfterUpdateStamina(reason, newStamina);
+ }
return newStamina;
}
@@ -141,7 +217,7 @@ public class StaminaManager {
if (!player.isPaused() && sustainedStaminaHandlerTimer == null) {
sustainedStaminaHandlerTimer = new Timer();
sustainedStaminaHandlerTimer.scheduleAtFixedRate(new SustainedStaminaHandler(), 0, 200);
- // Grasscutter.getLogger().debug("[MovementManager] SustainedStaminaHandlerTimer started");
+ Grasscutter.getLogger().debug("[MovementManager] SustainedStaminaHandlerTimer started");
}
}
@@ -149,7 +225,7 @@ public class StaminaManager {
if (sustainedStaminaHandlerTimer != null) {
sustainedStaminaHandlerTimer.cancel();
sustainedStaminaHandlerTimer = null;
- // Grasscutter.getLogger().debug("[MovementManager] SustainedStaminaHandlerTimer stopped");
+ Grasscutter.getLogger().debug("[MovementManager] SustainedStaminaHandlerTimer stopped");
}
}
@@ -188,17 +264,17 @@ public class StaminaManager {
switch (motionState) {
case MOTION_DASH_BEFORE_SHAKE:
if (previousState != MotionState.MOTION_DASH_BEFORE_SHAKE) {
- updateStamina(session, new Consumption(ConsumptionType.SPRINT));
+ updateStaminaRelative(session, new Consumption(ConsumptionType.SPRINT));
}
break;
case MOTION_CLIMB_JUMP:
if (previousState != MotionState.MOTION_CLIMB_JUMP) {
- updateStamina(session, new Consumption(ConsumptionType.CLIMB_JUMP));
+ updateStaminaRelative(session, new Consumption(ConsumptionType.CLIMB_JUMP));
}
break;
case MOTION_SWIM_DASH:
if (previousState != MotionState.MOTION_SWIM_DASH) {
- updateStamina(session, new Consumption(ConsumptionType.SWIM_DASH_START));
+ updateStaminaRelative(session, new Consumption(ConsumptionType.SWIM_DASH_START));
}
break;
}
@@ -206,7 +282,7 @@ public class StaminaManager {
private void handleImmediateStamina(GameSession session, EvtDoSkillSuccNotify notify) {
Consumption consumption = getFightConsumption(notify.getSkillId());
- updateStamina(session, consumption);
+ updateStaminaRelative(session, consumption);
}
private class SustainedStaminaHandler extends TimerTask {
@@ -216,22 +292,30 @@ public class StaminaManager {
int currentStamina = player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA);
int maxStamina = player.getProperty(PlayerProperty.PROP_MAX_STAMINA);
if (moving || (currentStamina < maxStamina)) {
- Grasscutter.getLogger().debug("Player moving: " + moving + ", stamina full: " +
+ Grasscutter.getLogger().trace("Player moving: " + moving + ", stamina full: " +
(currentStamina >= maxStamina) + ", recalculate stamina");
+
Consumption consumption = new Consumption(ConsumptionType.None);
- if (!isInSkillMove) {
- if (MotionStatesCategorized.get("CLIMB").contains(currentState)) {
- consumption = getClimbSustainedConsumption();
- } else if (MotionStatesCategorized.get("SWIM").contains((currentState))) {
- consumption = getSwimSustainedConsumptions();
- } else if (MotionStatesCategorized.get("RUN").contains(currentState)) {
- consumption = getRunWalkDashSustainedConsumption();
- } else if (MotionStatesCategorized.get("FLY").contains(currentState)) {
- consumption = getFlySustainedConsumption();
- } else if (MotionStatesCategorized.get("STANDBY").contains(currentState)) {
- consumption = getStandSustainedConsumption();
- }
+ if (MotionStatesCategorized.get("CLIMB").contains(currentState)) {
+ consumption = getClimbSustainedConsumption();
+ } else if (MotionStatesCategorized.get("SWIM").contains((currentState))) {
+ consumption = getSwimSustainedConsumptions();
+ } else if (MotionStatesCategorized.get("RUN").contains(currentState)) {
+ consumption = getRunWalkDashSustainedConsumption();
+ } else if (MotionStatesCategorized.get("FLY").contains(currentState)) {
+ consumption = getFlySustainedConsumption();
+ } else if (MotionStatesCategorized.get("STANDBY").contains(currentState)) {
+ consumption = getStandSustainedConsumption();
}
+
+ /*
+ TODO: Reductions that apply to all motion types:
+ Elemental Resonance
+ Wind: -15%
+ Skills
+ Diona E: -10% while shield lasts
+ Barbara E: -12% while lasts
+ */
if (cachedSession != null) {
if (consumption.amount < 0) {
staminaRecoverDelay = 0;
@@ -241,12 +325,12 @@ public class StaminaManager {
if (staminaRecoverDelay < 10) {
// For others recover after 2 seconds (10 ticks) - as official server does.
staminaRecoverDelay++;
- consumption = new Consumption(ConsumptionType.None);
+ consumption.amount = 0;
+ Grasscutter.getLogger().trace("[StaminaManager] Delaying recovery: " + staminaRecoverDelay);
}
}
- updateStamina(cachedSession, consumption);
+ updateStaminaRelative(cachedSession, consumption);
}
- handleDrowning();
}
}
previousState = currentState;
@@ -261,10 +345,9 @@ public class StaminaManager {
private void handleDrowning() {
int stamina = player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA);
if (stamina < 10) {
- boolean isSwimming = MotionStatesCategorized.get("SWIM").contains(currentState);
- Grasscutter.getLogger().debug(player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA) + "/" +
- player.getProperty(PlayerProperty.PROP_MAX_STAMINA) + "\t" + currentState + "\t" + isSwimming);
- if (isSwimming && currentState != MotionState.MOTION_SWIM_IDLE) {
+ Grasscutter.getLogger().trace(player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA) + "/" +
+ player.getProperty(PlayerProperty.PROP_MAX_STAMINA) + "\t" + currentState);
+ if (currentState != MotionState.MOTION_SWIM_IDLE) {
killAvatar(cachedSession, cachedEntity, PlayerDieType.PLAYER_DIE_DRAWN);
}
}
@@ -272,7 +355,33 @@ public class StaminaManager {
// Consumption Calculators
+ // Stamina Consumption Reduction: https://genshin-impact.fandom.com/wiki/Stamina
+
private Consumption getFightConsumption(int skillCasting) {
+ /* TODO:
+ Instead of handling here, consider call StaminaManager.updateStamina****() with a Consumption object with
+ type=FIGHT and a modified amount when handling attacks for more accurate attack start/end time and
+ other info. Handling it here could be very complicated.
+ Charged attack
+ Default:
+ Polearm: (-2500)
+ Claymore: (-4000 per second, -800 each tick)
+ Catalyst: (-5000)
+ Talent:
+ Ningguang: When Ningguang is in possession of Star Jades, her Charged Attack does not consume Stamina. (Catalyst * 0)
+ Klee: When Jumpy Dumpty and Normal Attacks deal DMG, Klee has a 50% chance to obtain an Explosive Spark.
+ This Explosive Spark is consumed by the next Charged Attack, which costs no Stamina. (Catalyst * 0)
+ Constellations:
+ Hu Tao: While in a Paramita Papilio state activated by Guide to Afterlife, Hu Tao's Charge Attacks do not consume Stamina. (Polearm * 0)
+ Character Specific:
+ Keqing: (-2500)
+ Diluc: (Claymore * 0.5)
+ Talent Moving: (Those are skills too)
+ Ayaka: (-1000 initial) (-1500 per second) When the Cryo application at the end of Kamisato Art: Senho hits an opponent (+1000)
+ Mona: (-1000 initial) (-1500 per second)
+ */
+
+ // TODO: Currently only handling Ayaka and Mona's talent moving initial costs.
Consumption consumption = new Consumption(ConsumptionType.None);
HashMap fightingCost = new HashMap<>() {{
put(10013, -1000); // Kamisato Ayaka
@@ -292,10 +401,12 @@ public class StaminaManager {
consumption = new Consumption(ConsumptionType.CLIMB_START);
}
}
+ // TODO: Foods
return consumption;
}
private Consumption getSwimSustainedConsumptions() {
+ handleDrowning();
Consumption consumption = new Consumption(ConsumptionType.None);
if (currentState == MotionState.MOTION_SWIM_MOVE) {
consumption = new Consumption(ConsumptionType.SWIMMING);
@@ -310,6 +421,7 @@ public class StaminaManager {
Consumption consumption = new Consumption(ConsumptionType.None);
if (currentState == MotionState.MOTION_DASH) {
consumption = new Consumption(ConsumptionType.DASH);
+ // TODO: Foods
}
if (currentState == MotionState.MOTION_RUN) {
consumption = new Consumption(ConsumptionType.RUN);
@@ -321,7 +433,12 @@ public class StaminaManager {
}
private Consumption getFlySustainedConsumption() {
+ // POWERED_FLY, e.g. wind tunnel
+ if (currentState == MotionState.MOTION_POWERED_FLY) {
+ return new Consumption(ConsumptionType.POWERED_FLY);
+ }
Consumption consumption = new Consumption(ConsumptionType.FLY);
+ // Talent
HashMap glidingCostReduction = new HashMap<>() {{
put(212301, 0.8f); // Amber
put(222301, 0.8f); // Venti
@@ -330,15 +447,15 @@ public class StaminaManager {
for (EntityAvatar entity : cachedSession.getPlayer().getTeamManager().getActiveTeam()) {
for (int skillId : entity.getAvatar().getProudSkillList()) {
if (glidingCostReduction.containsKey(skillId)) {
- reduction = glidingCostReduction.get(skillId);
+ float potentialLowerReduction = glidingCostReduction.get(skillId);
+ if (potentialLowerReduction < reduction) {
+ reduction = potentialLowerReduction;
+ }
}
}
}
consumption.amount *= reduction;
- // POWERED_FLY, e.g. wind tunnel
- if (currentState == MotionState.MOTION_POWERED_FLY) {
- consumption = new Consumption(ConsumptionType.POWERED_FLY);
- }
+ // TODO: Foods
return consumption;
}
diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerCombatInvocationsNotify.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerCombatInvocationsNotify.java
index cc9e7b345..36252f828 100644
--- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerCombatInvocationsNotify.java
+++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerCombatInvocationsNotify.java
@@ -49,22 +49,23 @@ public class HandlerCombatInvocationsNotify extends PacketHandler {
session.getPlayer().getStaminaManager().handleCombatInvocationsNotify(session, moveInfo, entity);
- // TODO: handle MOTION_FIGHT landing
- // For plunge attacks, LAND_SPEED is always -30 and is not useful.
- // May need the height when starting plunge attack.
+ // TODO: handle MOTION_FIGHT landing which has a different damage factor
+ // Also, for plunge attacks, LAND_SPEED is always -30 and is not useful.
+ // May need the height when starting plunge attack.
+ // MOTION_LAND_SPEED and MOTION_FALL_ON_GROUND arrive in different packets.
+ // Cache land speed for later use.
+ if (motionState == MotionState.MOTION_LAND_SPEED) {
+ cachedLandingSpeed = motionInfo.getSpeed().getY();
+ cachedLandingTimeMillisecond = System.currentTimeMillis();
+ monitorLandingEvent = true;
+ }
if (monitorLandingEvent) {
if (motionState == MotionState.MOTION_FALL_ON_GROUND) {
monitorLandingEvent = false;
handleFallOnGround(session, entity, motionState);
}
}
- if (motionState == MotionState.MOTION_LAND_SPEED) {
- // MOTION_LAND_SPEED and MOTION_FALL_ON_GROUND arrive in different packet. Cache land speed for later use.
- cachedLandingSpeed = motionInfo.getSpeed().getY();
- cachedLandingTimeMillisecond = System.currentTimeMillis();
- monitorLandingEvent = true;
- }
}
break;
default:
@@ -84,33 +85,42 @@ public class HandlerCombatInvocationsNotify extends PacketHandler {
}
private void handleFallOnGround(GameSession session, GameEntity entity, MotionState motionState) {
- // If not received immediately after MOTION_LAND_SPEED, discard this packet.
+ // People have reported that after plunge attack (client sends a FIGHT instead of FALL_ON_GROUND) they will die
+ // if they talk to an NPC (this is when the client sends a FALL_ON_GROUND) without jumping again.
+ // A dirty patch: if not received immediately after MOTION_LAND_SPEED, discard this packet.
+ // 200ms seems to be a reasonable delay.
int maxDelay = 200;
long actualDelay = System.currentTimeMillis() - cachedLandingTimeMillisecond;
- Grasscutter.getLogger().debug("MOTION_FALL_ON_GROUND received after " + actualDelay + "/" + maxDelay + "ms." + (actualDelay > maxDelay ? " Discard" : ""));
+ Grasscutter.getLogger().trace("MOTION_FALL_ON_GROUND received after " + actualDelay + "/" + maxDelay + "ms." + (actualDelay > maxDelay ? " Discard" : ""));
if (actualDelay > maxDelay) {
return;
}
float currentHP = entity.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP);
float maxHP = entity.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP);
- float damage = 0;
+ float damageFactor = 0;
if (cachedLandingSpeed < -23.5) {
- damage = (float) (maxHP * 0.33);
+ damageFactor = 0.33f;
}
if (cachedLandingSpeed < -25) {
- damage = (float) (maxHP * 0.5);
+ damageFactor = 0.5f;
}
if (cachedLandingSpeed < -26.5) {
- damage = (float) (maxHP * 0.66);
+ damageFactor = 0.66f;
}
if (cachedLandingSpeed < -28) {
- damage = (maxHP * 1);
+ damageFactor = 1f;
}
+ float damage = maxHP * damageFactor;
float newHP = currentHP - damage;
if (newHP < 0) {
newHP = 0;
}
- Grasscutter.getLogger().debug(currentHP + "/" + maxHP + "\t" + "\tDamage: " + damage + "\tnewHP: " + newHP);
+ if (damageFactor > 0) {
+ Grasscutter.getLogger().debug(currentHP + "/" + maxHP + "\tLandingSpeed: " + cachedLandingSpeed +
+ "\tDamageFactor: " + damageFactor + "\tDamage: " + damage + "\tNewHP: " + newHP);
+ } else {
+ Grasscutter.getLogger().trace(currentHP + "/" + maxHP + "\tLandingSpeed: 0\tNo damage");
+ }
entity.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, newHP);
entity.getWorld().broadcastPacket(new PacketEntityFightPropUpdateNotify(entity, FightProperty.FIGHT_PROP_CUR_HP));
if (newHP == 0) {
From 176f3e91f154ba5fe8b4b79fd68809d419478372 Mon Sep 17 00:00:00 2001
From: Michaellan <67815438+chrisblue@users.noreply.github.com>
Date: Sun, 8 May 2022 20:06:10 +0800
Subject: [PATCH 4/4] fill description
---
src/main/resources/languages/zh-CN.json | 105 ++++++++++++++++--------
1 file changed, 71 insertions(+), 34 deletions(-)
diff --git a/src/main/resources/languages/zh-CN.json b/src/main/resources/languages/zh-CN.json
index 8faa0e4ae..4e4929aee 100644
--- a/src/main/resources/languages/zh-CN.json
+++ b/src/main/resources/languages/zh-CN.json
@@ -95,17 +95,20 @@
"create": "已建立账号,UID 为 %s 。",
"delete": "账号已刪除。",
"no_account": "账号不存在。",
- "command_usage": "用法:account [uid]"
+ "command_usage": "用法:account [uid]",
+ "description": "创建或删除账号。"
},
"broadcast": {
"command_usage": "用法:broadcast <消息>",
- "message_sent": "公告已发送。"
+ "message_sent": "公告已发送。",
+ "description": "向所有玩家发送公告。"
},
"changescene": {
"usage": "用法:changescene ",
"already_in_scene": "你已经在这个秘境中了。",
"success": "已切换至秘境 %s.",
- "exists_error": "此秘境不存在。"
+ "exists_error": "此秘境不存在。",
+ "description": "切换指定秘境。"
},
"clear": {
"command_usage": "用法: clear ",
@@ -115,35 +118,41 @@
"furniture": "已将 %s 的尘歌壶家具清空。",
"displays": "已清除 %s 的显示。",
"virtuals": "已将 %s 的所有货币和经验值清空。",
- "everything": "已将 %s 的所有物品清空。"
+ "everything": "已将 %s 的所有物品清空。",
+ "description": "从您的背包中删除所有未装备且已解锁的物品,包括稀有物品。"
},
"coop": {
"usage": "用法:coop ",
- "success": "已强制召唤 %s 到 %s的世界"
+ "success": "已强制召唤 %s 到 %s的世界",
+ "description": "强制召唤指定用户到他人的世界。"
},
"enter_dungeon": {
"usage": "用法:enterdungeon ",
"changed": "已进入秘境 %s",
"not_found_error": "此秘境不存在。",
- "in_dungeon_error": "你已经在秘境中了。"
+ "in_dungeon_error": "你已经在秘境中了。",
+ "description": "进入指定秘境。"
},
"giveAll": {
"usage": "用法:giveall [player] [amount]",
"started": "正在给予全部物品...",
"success": "已给予全部物品。",
- "invalid_amount_or_playerId": "无效的数量/玩家ID。"
+ "invalid_amount_or_playerId": "无效的数量/玩家ID。",
+ "description": "给予所有物品。"
},
"giveArtifact": {
"usage": "用法:giveart|gart [player] [[,]]... [level]",
"id_error": "无效的圣遗物ID。",
- "success": "已将 %s 给予 %s。"
+ "success": "已将 %s 给予 %s。",
+ "description": "给予指定圣遗物。"
},
"giveChar": {
"usage": "用法:givechar [amount]",
"given": "给予角色 %s 等级 %s 向UID %s.",
"invalid_avatar_id": "无效的角色ID。",
"invalid_avatar_level": "无效的角色等級。.",
- "invalid_avatar_or_player_id": "无效的角色ID/玩家ID。"
+ "invalid_avatar_or_player_id": "无效的角色ID/玩家ID。",
+ "description": "给予指定角色。"
},
"give": {
"usage": "用法:give [amount] [level] [refinement]",
@@ -151,29 +160,36 @@
"refinement_must_between_1_and_5": "精炼等阶必须在 1 到 5 之间。",
"given": "已将 %s 个 %s 给予 %s。",
"given_with_level_and_refinement": "已将 %s [等級%s, 精炼%s] %s个给予 %s",
- "given_level": "已将 %s 等级 %s %s 个给予UID %s"
+ "given_level": "已将 %s 等级 %s %s 个给予UID %s",
+ "description": "给予指定物品。"
},
"godmode": {
- "success": "上帝模式已被设置为 %s 。 [用户:%s]"
+ "success": "上帝模式已被设置为 %s 。 [用户:%s]",
+ "description": "防止你受到伤害。"
},
"heal": {
- "success": "所有角色已被治疗。"
+ "success": "所有角色已被治疗。",
+ "description": "治疗所选队伍的角色。"
},
"kick": {
"player_kick_player": "玩家 [%s:%s] 已将 [%s:%s] 踢出",
- "server_kick_player": "正在踢出玩家 [%s:%s]"
+ "server_kick_player": "正在踢出玩家 [%s:%s]",
+ "description": "从服务器内踢出指定玩家。"
},
"kill": {
"usage": "用法:killall [playerUid] [sceneId]",
"scene_not_found_in_player_world": "未在玩家世界中找到此场景",
- "kill_monsters_in_scene": "已杀死 %s 个怪物。 [场景ID: %s]"
+ "kill_monsters_in_scene": "已杀死 %s 个怪物。 [场景ID: %s]",
+ "description": "杀死所有怪物"
},
"killCharacter": {
"usage": "用法:/killcharacter [playerId]",
- "success": "已杀死 %s 目前使用的角色。"
+ "success": "已杀死 %s 目前使用的角色。",
+ "description": "杀死目前使用的角色"
},
"list": {
- "success": "目前在线人数:%s"
+ "success": "目前在线人数:%s",
+ "description": "查看所有玩家"
},
"permission": {
"usage": "用法:permission ",
@@ -181,21 +197,26 @@
"has_error": "此玩家已拥有此权限!",
"remove": "权限已移除。",
"not_have_error": "此玩家未拥有权限!",
- "account_error": "账号不存在!"
+ "account_error": "账号不存在!",
+ "description": "给予或移除指定玩家的权限。"
},
"position": {
- "success": "坐标:%.3f, %.3f, %.3f\n场景ID:%d"
+ "success": "坐标:%.3f, %.3f, %.3f\n场景ID:%d",
+ "description": "获取所在位置。"
},
"reload": {
"reload_start": "正在重载配置文件和数据。",
- "reload_done": "重载完毕。"
+ "reload_done": "重载完毕。",
+ "description": "重载配置文件和数据。"
},
"resetConst": {
"reset_all": "重置所有角色的命座。",
- "success": "已重置 %s 的命座,重新登录后将会生效。"
+ "success": "已重置 %s 的命座,重新登录后将会生效。",
+ "description": "重置当前角色的命之座,执行命令后需重新登录以生效。"
},
"resetShopLimit": {
- "usage": "用法:/resetshop "
+ "usage": "用法:/resetshop ",
+ "description": "重置所选玩家的商店刷新时间。"
},
"sendMail": {
"usage": "用法:give [player] [amount]",
@@ -217,17 +238,20 @@
"message": "<正文>",
"sender": "<发件人>",
"arguments": " [数量] [等级]",
- "error": "错误:无效的编写阶段 %s。需要 StackTrace 请查看服务器控制台。"
+ "error": "错误:无效的编写阶段 %s。需要 StackTrace 请查看服务器控制台。",
+ "description": "向指定用户发送邮件。 此命令的用法可根据附加的参数而变化。"
},
"sendMessage": {
"usage": "用法:sendmessage ",
- "success": "消息已发送。"
+ "success": "消息已发送。",
+ "description": "向指定玩家发送消息"
},
"setFetterLevel": {
"usage": "用法:setfetterlevel ",
"range_error": "好感度等级必须在 0 到 10 之间。",
"fetter_set_level": "好感度已设置为 %s 级",
- "level_error": "无效的好感度等级。"
+ "level_error": "无效的好感度等级。",
+ "description": "设置当前角色的好感度等级。"
},
"setStats": {
"usage_console": "用法:setstats|stats @ ",
@@ -238,20 +262,24 @@
"player_error": "玩家不存在或已离线。",
"set_self": "%s 已经设置为 %s。",
"set_for_uid": "%s 的使用者 %s 更改为 %s。",
- "set_max_hp": "最大生命值更改为 %s。"
+ "set_max_hp": "最大生命值更改为 %s。",
+ "description": "设置当前角色的属性。"
},
"setWorldLevel": {
"usage": "用法:setworldlevel ",
"value_error": "世界等级必须设置在0-8之间。",
"success": "已将世界等级设为%s。",
- "invalid_world_level": "无效的世界等级。"
+ "invalid_world_level": "无效的世界等级。",
+ "description": "设置世界等级,执行命令后需重新登录以生效。"
},
"spawn": {
"usage": "用法:spawn [amount] [level(仅限怪物]",
- "success": "已生成 %s 个 %s。"
+ "success": "已生成 %s 个 %s。",
+ "description": "在你附近生成一个生物。"
},
"stop": {
- "success": "正在关闭服务器..."
+ "success": "正在关闭服务器...",
+ "description": "停止服务器"
},
"talent": {
"usage_1": "设置天赋等级:/talent set ",
@@ -267,32 +295,41 @@
"invalid_level": "无效的天赋等级。",
"normal_attack_id": "普通攻击的 ID 为 %s。",
"e_skill_id": "元素战技ID %s。",
- "q_skill_id": "元素爆发ID %s。"
+ "q_skill_id": "元素爆发ID %s。",
+ "description": "设置当前角色的天赋等级。"
},
"teleportAll": {
"success": "已将全部玩家传送到你的位置",
- "error": "命令仅限处于多人游戏状态下使用。"
+ "error": "命令仅限处于多人游戏状态下使用。",
+ "description": "将你世界中的所有玩家传送到你所在的位置。"
},
"teleport": {
"usage_server": "用法:/tp @ [scene id]",
"usage": "用法:/tp [@] [scene id]",
"specify_player_id": "你必须指定一个玩家ID。",
"invalid_position": "无效的位置。",
- "success": "传送 %s 到坐标 %s,%s,%s,场景为 %s"
+ "success": "传送 %s 到坐标 %s,%s,%s,场景为 %s",
+ "description": "改变指定玩家的位置。"
},
"weather": {
"usage": "用法:weather [climateId]",
"success": "已将当前天气设定为 %s,气候为 %s。",
- "invalid_id": "无效的天气ID。"
+ "invalid_id": "无效的天气ID。",
+ "description": "改变天气"
},
"drop": {
"command_usage": "用法:drop [amount]",
- "success": "已将 %s x %s 丟在附近。"
+ "success": "已将 %s x %s 丟在附近。",
+ "description": "在你附近丢一个物品。"
},
"help": {
"usage": "用法:",
"aliases": "別名:",
- "available_commands": "可用指令:"
+ "available_commands": "可用指令:",
+ "description": "发送帮助信息或显示指定命令的信息。"
+ },
+ "restart": {
+ "description": "重新启动服务器。"
}
}
}