Commit 219a8508 authored by Akka's avatar Akka
Browse files

Merge remote-tracking branch 'origin/development' into tower

parents 4b6842f0 65861c3c
...@@ -10,9 +10,9 @@ import java.util.List; ...@@ -10,9 +10,9 @@ import java.util.List;
import static emu.grasscutter.utils.Language.translate; import static emu.grasscutter.utils.Language.translate;
@Command(label = "tpall", usage = "tpall", @Command(label = "tpall", usage = "tpall", permission = "player.tpall", description = "commands.teleportAll.description")
description = "Teleports all players in your world to your position", permission = "player.tpall")
public final class TeleportAllCommand implements CommandHandler { public final class TeleportAllCommand implements CommandHandler {
@Override @Override
public void execute(Player sender, Player targetPlayer, List<String> args) { public void execute(Player sender, Player targetPlayer, List<String> args) {
if (targetPlayer == null) { if (targetPlayer == null) {
......
...@@ -10,8 +10,7 @@ import java.util.List; ...@@ -10,8 +10,7 @@ import java.util.List;
import static emu.grasscutter.utils.Language.translate; import static emu.grasscutter.utils.Language.translate;
@Command(label = "teleport", usage = "teleport <x> <y> <z> [scene id]", aliases = {"tp"}, @Command(label = "teleport", usage = "teleport <x> <y> <z> [scene id]", aliases = {"tp"}, permission = "player.teleport", description = "commands.teleport.description")
description = "Change the player's position.", permission = "player.teleport")
public final class TeleportCommand implements CommandHandler { public final class TeleportCommand implements CommandHandler {
private float parseRelative(String input, Float current) { // TODO: Maybe this will be useful elsewhere later private float parseRelative(String input, Float current) { // TODO: Maybe this will be useful elsewhere later
......
...@@ -11,8 +11,7 @@ import java.util.List; ...@@ -11,8 +11,7 @@ import java.util.List;
import static emu.grasscutter.utils.Language.translate; import static emu.grasscutter.utils.Language.translate;
@Command(label = "weather", usage = "weather <weatherId> [climateId]", @Command(label = "weather", usage = "weather <weatherId> [climateId]", aliases = {"w"}, permission = "player.weather", description = "commands.weather.description")
description = "Changes the weather.", aliases = {"w"}, permission = "player.weather")
public final class WeatherCommand implements CommandHandler { public final class WeatherCommand implements CommandHandler {
@Override @Override
......
...@@ -96,7 +96,7 @@ public class GachaBanner { ...@@ -96,7 +96,7 @@ public class GachaBanner {
return toProto(""); return toProto("");
} }
public GachaInfo toProto(String sessionKey) { public GachaInfo toProto(String sessionKey) {
String record = "https://" String record = "http" + (Grasscutter.getConfig().getDispatchOptions().FrontHTTPS ? "s" : "") + "://"
+ (Grasscutter.getConfig().getDispatchOptions().PublicIp.isEmpty() ? + (Grasscutter.getConfig().getDispatchOptions().PublicIp.isEmpty() ?
Grasscutter.getConfig().getDispatchOptions().Ip : Grasscutter.getConfig().getDispatchOptions().Ip :
Grasscutter.getConfig().getDispatchOptions().PublicIp) Grasscutter.getConfig().getDispatchOptions().PublicIp)
......
package emu.grasscutter.game.managers.SotSManager; package emu.grasscutter.game.managers;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
import emu.grasscutter.game.avatar.Avatar; import emu.grasscutter.game.avatar.Avatar;
import emu.grasscutter.game.entity.EntityAvatar; import emu.grasscutter.game.entity.EntityAvatar;
import emu.grasscutter.game.entity.GameEntity;
import emu.grasscutter.game.managers.MovementManager.MovementManager;
import emu.grasscutter.game.player.Player; import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.props.FightProperty; import emu.grasscutter.game.props.FightProperty;
import emu.grasscutter.game.props.PlayerProperty; import emu.grasscutter.game.props.PlayerProperty;
import emu.grasscutter.game.world.World;
import emu.grasscutter.net.proto.ChangeHpReasonOuterClass; import emu.grasscutter.net.proto.ChangeHpReasonOuterClass;
import emu.grasscutter.net.proto.PropChangeReasonOuterClass; import emu.grasscutter.net.proto.PropChangeReasonOuterClass;
import emu.grasscutter.server.game.GameSession; import emu.grasscutter.server.game.GameSession;
...@@ -29,6 +26,8 @@ public class SotSManager { ...@@ -29,6 +26,8 @@ public class SotSManager {
private final Player player; private final Player player;
private Timer autoRecoverTimer; private Timer autoRecoverTimer;
public final static int GlobalMaximumSpringVolume = 8500000;
public SotSManager(Player player) { public SotSManager(Player player) {
this.player = player; this.player = player;
} }
......
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);
}
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
package emu.grasscutter.game.managers.StaminaManager;
public class Consumption {
public ConsumptionType consumptionType;
public int amount;
public Consumption(ConsumptionType ct, int a) {
consumptionType = ct;
amount = a;
}
public Consumption(ConsumptionType ct) {
this(ct, ct.amount);
}
}
package emu.grasscutter.game.managers.StaminaManager;
public enum ConsumptionType {
None(0),
// consume
CLIMB_START(-500),
CLIMBING(-150),
CLIMB_JUMP(-2500),
SPRINT(-1800),
DASH(-360),
FLY(-60),
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),
RUN(500),
WALK(500),
STANDBY_MOVE(500),
POWERED_FLY(500);
public final int amount;
ConsumptionType(int amount) {
this.amount = amount;
}
}
\ No newline at end of file
# 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
package emu.grasscutter.game.managers.MovementManager; package emu.grasscutter.game.managers.StaminaManager;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
import emu.grasscutter.game.entity.EntityAvatar; import emu.grasscutter.game.entity.EntityAvatar;
...@@ -7,374 +7,421 @@ import emu.grasscutter.game.player.Player; ...@@ -7,374 +7,421 @@ import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.props.FightProperty; import emu.grasscutter.game.props.FightProperty;
import emu.grasscutter.game.props.LifeState; import emu.grasscutter.game.props.LifeState;
import emu.grasscutter.game.props.PlayerProperty; import emu.grasscutter.game.props.PlayerProperty;
import emu.grasscutter.net.proto.EntityMoveInfoOuterClass; import emu.grasscutter.net.proto.EntityMoveInfoOuterClass.EntityMoveInfo;
import emu.grasscutter.net.proto.EvtDoSkillSuccNotifyOuterClass.EvtDoSkillSuccNotify;
import emu.grasscutter.net.proto.MotionInfoOuterClass.MotionInfo; import emu.grasscutter.net.proto.MotionInfoOuterClass.MotionInfo;
import emu.grasscutter.net.proto.MotionStateOuterClass.MotionState; import emu.grasscutter.net.proto.MotionStateOuterClass.MotionState;
import emu.grasscutter.net.proto.PlayerDieTypeOuterClass.PlayerDieType; import emu.grasscutter.net.proto.PlayerDieTypeOuterClass.PlayerDieType;
import emu.grasscutter.net.proto.VectorOuterClass; import emu.grasscutter.net.proto.VectorOuterClass.Vector;
import emu.grasscutter.server.game.GameSession; import emu.grasscutter.server.game.GameSession;
import emu.grasscutter.server.packet.send.*; import emu.grasscutter.server.packet.send.*;
import emu.grasscutter.utils.Position; import emu.grasscutter.utils.Position;
import org.jetbrains.annotations.NotNull;
import java.lang.Math; import java.lang.Math;
import java.util.*; import java.util.*;
public class MovementManager { public class StaminaManager {
public HashMap<String, HashSet<MotionState>> MotionStatesCategorized = new HashMap<>();
private enum ConsumptionType {
None(0),
// consume
CLIMB_START(-500),
CLIMBING(-150),
CLIMB_JUMP(-2500),
DASH(-1800),
SPRINT(-360),
FLY(-60),
SWIM_DASH_START(-200),
SWIM_DASH(-200),
SWIMMING(-80),
FIGHT(0),
// restore
STANDBY(500),
RUN(500),
WALK(500),
STANDBY_MOVE(500),
POWERED_FLY(500);
public final int amount;
ConsumptionType(int amount) {
this.amount = amount;
}
}
private class Consumption {
public ConsumptionType consumptionType;
public int amount;
public Consumption(ConsumptionType ct, int a) {
consumptionType = ct;
amount = a;
}
public Consumption(ConsumptionType ct) {
this(ct, ct.amount);
}
}
private MotionState previousState = MotionState.MOTION_STANDBY;
private MotionState currentState = MotionState.MOTION_STANDBY;
private Position previousCoordinates = new Position(0, 0, 0);
private Position currentCoordinates = new Position(0, 0, 0);
private final Player player; private final Player player;
private HashMap<String, HashSet<MotionState>> MotionStatesCategorized = new HashMap<>();
private float landSpeed = 0; public final static int GlobalMaximumStamina = 24000;
private long landTimeMillisecond = 0; private Position currentCoordinates = new Position(0, 0, 0);
private Timer movementManagerTickTimer; private Position previousCoordinates = new Position(0, 0, 0);
private MotionState currentState = MotionState.MOTION_STANDBY;
private MotionState previousState = MotionState.MOTION_STANDBY;
private Timer sustainedStaminaHandlerTimer;
private GameSession cachedSession = null; private GameSession cachedSession = null;
private GameEntity cachedEntity = null; private GameEntity cachedEntity = null;
private int staminaRecoverDelay = 0; private int staminaRecoverDelay = 0;
private int skillCaster = 0;
private int skillCasting = 0;
public MovementManager(Player player) { private HashMap<String, BeforeUpdateStaminaListener> beforeUpdateStaminaListeners = new HashMap<>();
previousCoordinates.add(new Position(0,0,0)); private HashMap<String, AfterUpdateStaminaListener> afterUpdateStaminaListeners = new HashMap<>();
public StaminaManager(Player player) {
this.player = player; this.player = player;
MotionStatesCategorized.put("SWIM", new HashSet<>(Arrays.asList( MotionStatesCategorized.put("SWIM", new HashSet<>(Arrays.asList(
MotionState.MOTION_SWIM_MOVE, MotionState.MOTION_SWIM_MOVE,
MotionState.MOTION_SWIM_IDLE, MotionState.MOTION_SWIM_IDLE,
MotionState.MOTION_SWIM_DASH, MotionState.MOTION_SWIM_DASH,
MotionState.MOTION_SWIM_JUMP MotionState.MOTION_SWIM_JUMP
))); )));
MotionStatesCategorized.put("STANDBY", new HashSet<>(Arrays.asList( MotionStatesCategorized.put("STANDBY", new HashSet<>(Arrays.asList(
MotionState.MOTION_STANDBY, MotionState.MOTION_STANDBY,
MotionState.MOTION_STANDBY_MOVE, MotionState.MOTION_STANDBY_MOVE,
MotionState.MOTION_DANGER_STANDBY, MotionState.MOTION_DANGER_STANDBY,
MotionState.MOTION_DANGER_STANDBY_MOVE, MotionState.MOTION_DANGER_STANDBY_MOVE,
MotionState.MOTION_LADDER_TO_STANDBY, MotionState.MOTION_LADDER_TO_STANDBY,
MotionState.MOTION_JUMP_UP_WALL_FOR_STANDBY MotionState.MOTION_JUMP_UP_WALL_FOR_STANDBY
))); )));
MotionStatesCategorized.put("CLIMB", new HashSet<>(Arrays.asList( MotionStatesCategorized.put("CLIMB", new HashSet<>(Arrays.asList(
MotionState.MOTION_CLIMB, MotionState.MOTION_CLIMB,
MotionState.MOTION_CLIMB_JUMP, MotionState.MOTION_CLIMB_JUMP,
MotionState.MOTION_STANDBY_TO_CLIMB, MotionState.MOTION_STANDBY_TO_CLIMB,
MotionState.MOTION_LADDER_IDLE, MotionState.MOTION_LADDER_IDLE,
MotionState.MOTION_LADDER_MOVE, MotionState.MOTION_LADDER_MOVE,
MotionState.MOTION_LADDER_SLIP, MotionState.MOTION_LADDER_SLIP,
MotionState.MOTION_STANDBY_TO_LADDER MotionState.MOTION_STANDBY_TO_LADDER
))); )));
MotionStatesCategorized.put("FLY", new HashSet<>(Arrays.asList( MotionStatesCategorized.put("FLY", new HashSet<>(Arrays.asList(
MotionState.MOTION_FLY, MotionState.MOTION_FLY,
MotionState.MOTION_FLY_IDLE, MotionState.MOTION_FLY_IDLE,
MotionState.MOTION_FLY_SLOW, MotionState.MOTION_FLY_SLOW,
MotionState.MOTION_FLY_FAST, MotionState.MOTION_FLY_FAST,
MotionState.MOTION_POWERED_FLY MotionState.MOTION_POWERED_FLY
))); )));
MotionStatesCategorized.put("RUN", new HashSet<>(Arrays.asList( MotionStatesCategorized.put("RUN", new HashSet<>(Arrays.asList(
MotionState.MOTION_DASH, MotionState.MOTION_DASH,
MotionState.MOTION_DANGER_DASH, MotionState.MOTION_DANGER_DASH,
MotionState.MOTION_DASH_BEFORE_SHAKE, MotionState.MOTION_DASH_BEFORE_SHAKE,
MotionState.MOTION_RUN, MotionState.MOTION_RUN,
MotionState.MOTION_DANGER_RUN, MotionState.MOTION_DANGER_RUN,
MotionState.MOTION_WALK, MotionState.MOTION_WALK,
MotionState.MOTION_DANGER_WALK MotionState.MOTION_DANGER_WALK
))); )));
MotionStatesCategorized.put("FIGHT", new HashSet<>(Arrays.asList( 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
)));
} }
public void handle(GameSession session, EntityMoveInfoOuterClass.EntityMoveInfo moveInfo, GameEntity entity) { // Listeners
if (movementManagerTickTimer == null) {
movementManagerTickTimer = new Timer(); public boolean registerBeforeUpdateStaminaListener(String listenerName, BeforeUpdateStaminaListener listener) {
movementManagerTickTimer.scheduleAtFixedRate(new MotionManagerTick(), 0, 200); if (beforeUpdateStaminaListeners.containsKey(listenerName)) {
return false;
} }
// cache info for later use in tick beforeUpdateStaminaListeners.put(listenerName, listener);
cachedSession = session; return true;
cachedEntity = entity; }
MotionInfo motionInfo = moveInfo.getMotionInfo(); public boolean unregisterBeforeUpdateStaminaListener(String listenerName) {
moveEntity(entity, moveInfo); if (!beforeUpdateStaminaListeners.containsKey(listenerName)) {
VectorOuterClass.Vector posVector = motionInfo.getPos(); return false;
Position newPos = new Position(posVector.getX(),
posVector.getY(), posVector.getZ());;
if (newPos.getX() != 0 && newPos.getY() != 0 && newPos.getZ() != 0) {
currentCoordinates = newPos;
} }
currentState = motionInfo.getState(); beforeUpdateStaminaListeners.remove(listenerName);
Grasscutter.getLogger().debug("" + currentState + "\t" + (moveInfo.getIsReliable() ? "reliable" : "")); return true;
handleFallOnGround(motionInfo);
} }
public void resetTimer() { public boolean registerAfterUpdateStaminaListener(String listenerName, AfterUpdateStaminaListener listener) {
Grasscutter.getLogger().debug("MovementManager ticker stopped"); if (afterUpdateStaminaListeners.containsKey(listenerName)) {
movementManagerTickTimer.cancel(); return false;
movementManagerTickTimer = null; }
afterUpdateStaminaListeners.put(listenerName, listener);
return true;
} }
private void moveEntity(GameEntity entity, EntityMoveInfoOuterClass.EntityMoveInfo moveInfo) { public boolean unregisterAfterUpdateStaminaListener(String listenerName) {
entity.getPosition().set(moveInfo.getMotionInfo().getPos()); if (!afterUpdateStaminaListeners.containsKey(listenerName)) {
entity.getRotation().set(moveInfo.getMotionInfo().getRot()); return false;
entity.setLastMoveSceneTimeMs(moveInfo.getSceneTime()); }
entity.setLastMoveReliableSeq(moveInfo.getReliableSeq()); afterUpdateStaminaListeners.remove(listenerName);
entity.setMotionState(moveInfo.getMotionInfo().getState()); return true;
} }
private boolean isPlayerMoving() { private boolean isPlayerMoving() {
float diffX = currentCoordinates.getX() - previousCoordinates.getX(); float diffX = currentCoordinates.getX() - previousCoordinates.getX();
float diffY = currentCoordinates.getY() - previousCoordinates.getY(); float diffY = currentCoordinates.getY() - previousCoordinates.getY();
float diffZ = currentCoordinates.getZ() - previousCoordinates.getZ(); float diffZ = currentCoordinates.getZ() - previousCoordinates.getZ();
// Grasscutter.getLogger().debug("isPlayerMoving: " + previousCoordinates + ", " + currentCoordinates + ", " + diffX + ", " + diffY + ", " + diffZ); Grasscutter.getLogger().trace("isPlayerMoving: " + previousCoordinates + ", " + currentCoordinates +
return Math.abs(diffX) > 0.2 || Math.abs(diffY) > 0.1 || Math.abs(diffZ) > 0.2; ", " + diffX + ", " + diffY + ", " + diffZ);
} return Math.abs(diffX) > 0.3 || Math.abs(diffY) > 0.2 || Math.abs(diffZ) > 0.3;
private int getCurrentStamina() {
return player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA);
} }
private int getMaximumStamina() { public int updateStaminaRelative(GameSession session, Consumption consumption) {
return player.getProperty(PlayerProperty.PROP_MAX_STAMINA); int currentStamina = player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA);
} if (consumption.amount == 0) {
// Returns new stamina
public int updateStamina(GameSession session, int amount) {
int currentStamina = session.getPlayer().getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA);
if (amount == 0) {
return currentStamina; return currentStamina;
} }
int playerMaxStamina = session.getPlayer().getProperty(PlayerProperty.PROP_MAX_STAMINA); // notify will update
int newStamina = currentStamina + amount; for (Map.Entry<String, BeforeUpdateStaminaListener> 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().trace(currentStamina + "/" + playerMaxStamina + "\t" + currentState + "\t" +
(isPlayerMoving() ? "moving" : " ") + "\t(" + consumption.consumptionType + "," +
consumption.amount + ")");
int newStamina = currentStamina + consumption.amount;
if (newStamina < 0) { if (newStamina < 0) {
newStamina = 0; newStamina = 0;
} 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<String, BeforeUpdateStaminaListener> 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;
}
} }
if (newStamina > playerMaxStamina) { int playerMaxStamina = player.getProperty(PlayerProperty.PROP_MAX_STAMINA);
if (newStamina < 0) {
newStamina = 0;
} else if (newStamina > playerMaxStamina) {
newStamina = playerMaxStamina; newStamina = playerMaxStamina;
} }
session.getPlayer().setProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA, newStamina); 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)); session.send(new PacketPlayerPropNotify(player, PlayerProperty.PROP_CUR_PERSIST_STAMINA));
// notify updated
for (Map.Entry<String, AfterUpdateStaminaListener> listener : afterUpdateStaminaListeners.entrySet()) {
listener.getValue().onAfterUpdateStamina(reason, newStamina);
}
return newStamina; return newStamina;
} }
private void handleFallOnGround(@NotNull MotionInfo motionInfo) { // Kills avatar, removes entity and sends notification.
MotionState state = motionInfo.getState(); // TODO: Probably move this to Avatar class? since other components may also need to kill avatar.
// land speed and fall on ground event arrive in different packets public void killAvatar(GameSession session, GameEntity entity, PlayerDieType dieType) {
// cache land speed session.send(new PacketAvatarLifeStateChangeNotify(player.getTeamManager().getCurrentAvatarEntity().getAvatar(),
if (state == MotionState.MOTION_LAND_SPEED) { LifeState.LIFE_DEAD, dieType));
landSpeed = motionInfo.getSpeed().getY(); session.send(new PacketLifeStateChangeNotify(entity, LifeState.LIFE_DEAD, dieType));
landTimeMillisecond = System.currentTimeMillis(); entity.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, 0);
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);
}
public void startSustainedStaminaHandler() {
if (!player.isPaused() && sustainedStaminaHandlerTimer == null) {
sustainedStaminaHandlerTimer = new Timer();
sustainedStaminaHandlerTimer.scheduleAtFixedRate(new SustainedStaminaHandler(), 0, 200);
Grasscutter.getLogger().debug("[MovementManager] SustainedStaminaHandlerTimer started");
} }
if (state == MotionState.MOTION_FALL_ON_GROUND) { }
// if not received immediately after MOTION_LAND_SPEED, discard this packet.
// TODO: Test in high latency. public void stopSustainedStaminaHandler() {
int maxDelay = 200; if (sustainedStaminaHandlerTimer != null) {
if ((System.currentTimeMillis() - landTimeMillisecond) > maxDelay) { sustainedStaminaHandlerTimer.cancel();
Grasscutter.getLogger().debug("MOTION_FALL_ON_GROUND received after " + maxDelay + "ms, discard."); sustainedStaminaHandlerTimer = null;
return; Grasscutter.getLogger().debug("[MovementManager] SustainedStaminaHandlerTimer stopped");
}
float currentHP = cachedEntity.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP);
float maxHP = cachedEntity.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP);
float damage = 0;
Grasscutter.getLogger().debug("LandSpeed: " + landSpeed);
if (landSpeed < -23.5) {
damage = (float)(maxHP * 0.33);
}
if (landSpeed < -25) {
damage = (float)(maxHP * 0.5);
}
if (landSpeed < -26.5) {
damage = (float)(maxHP * 0.66);
}
if (landSpeed < -28) {
damage = (maxHP * 1);
}
float newHP = currentHP - damage;
if (newHP < 0) {
newHP = 0;
}
Grasscutter.getLogger().debug("Max: " + maxHP + "\tCurr: " + currentHP + "\tDamage: " + damage + "\tnewHP: " + newHP);
cachedEntity.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, newHP);
cachedEntity.getWorld().broadcastPacket(new PacketEntityFightPropUpdateNotify(cachedEntity, FightProperty.FIGHT_PROP_CUR_HP));
if (newHP == 0) {
killAvatar(cachedSession, cachedEntity, PlayerDieType.PLAYER_DIE_FALL);
}
landSpeed = 0;
} }
} }
private void handleDrowning() { // Handlers
int stamina = getCurrentStamina();
if (stamina < 10) { // External trigger handler
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); public void handleEvtDoSkillSuccNotify(GameSession session, EvtDoSkillSuccNotify notify) {
if (isSwimming && currentState != MotionState.MOTION_SWIM_IDLE) { handleImmediateStamina(session, notify);
killAvatar(cachedSession, cachedEntity, PlayerDieType.PLAYER_DIE_DRAWN); }
public void handleCombatInvocationsNotify(GameSession session, EntityMoveInfo moveInfo, GameEntity entity) {
// cache info for later use in SustainedStaminaHandler tick
cachedSession = session;
cachedEntity = entity;
MotionInfo motionInfo = moveInfo.getMotionInfo();
MotionState motionState = motionInfo.getState();
boolean isReliable = moveInfo.getIsReliable();
Grasscutter.getLogger().trace("" + motionState + "\t" + (isReliable ? "reliable" : ""));
if (isReliable) {
currentState = motionState;
Vector posVector = motionInfo.getPos();
Position newPos = new Position(posVector.getX(), posVector.getY(), posVector.getZ());
if (newPos.getX() != 0 && newPos.getY() != 0 && newPos.getZ() != 0) {
currentCoordinates = newPos;
} }
} }
startSustainedStaminaHandler();
handleImmediateStamina(session, motionInfo, motionState, entity);
} }
public void killAvatar(GameSession session, GameEntity entity, PlayerDieType dieType) { // Internal handler
cachedSession.send(new PacketAvatarLifeStateChangeNotify(
cachedSession.getPlayer().getTeamManager().getCurrentAvatarEntity().getAvatar(), private void handleImmediateStamina(GameSession session, MotionInfo motionInfo, MotionState motionState,
LifeState.LIFE_DEAD, GameEntity entity) {
dieType switch (motionState) {
)); case MOTION_DASH_BEFORE_SHAKE:
cachedSession.send(new PacketLifeStateChangeNotify( if (previousState != MotionState.MOTION_DASH_BEFORE_SHAKE) {
cachedEntity, updateStaminaRelative(session, new Consumption(ConsumptionType.SPRINT));
LifeState.LIFE_DEAD, }
dieType break;
)); case MOTION_CLIMB_JUMP:
cachedEntity.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, 0); if (previousState != MotionState.MOTION_CLIMB_JUMP) {
cachedEntity.getWorld().broadcastPacket(new PacketEntityFightPropUpdateNotify(cachedEntity, FightProperty.FIGHT_PROP_CUR_HP)); updateStaminaRelative(session, new Consumption(ConsumptionType.CLIMB_JUMP));
entity.getWorld().broadcastPacket(new PacketLifeStateChangeNotify(0, entity, LifeState.LIFE_DEAD)); }
session.getPlayer().getScene().removeEntity(entity); break;
((EntityAvatar)entity).onDeath(dieType, 0); case MOTION_SWIM_DASH:
if (previousState != MotionState.MOTION_SWIM_DASH) {
updateStaminaRelative(session, new Consumption(ConsumptionType.SWIM_DASH_START));
}
break;
}
}
private void handleImmediateStamina(GameSession session, EvtDoSkillSuccNotify notify) {
Consumption consumption = getFightConsumption(notify.getSkillId());
updateStaminaRelative(session, consumption);
} }
private class MotionManagerTick extends TimerTask private class SustainedStaminaHandler extends TimerTask {
{
public void run() { public void run() {
if (Grasscutter.getConfig().OpenStamina) { if (Grasscutter.getConfig().OpenStamina) {
boolean moving = isPlayerMoving(); boolean moving = isPlayerMoving();
if (moving || (getCurrentStamina() < getMaximumStamina())) { int currentStamina = player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA);
// Grasscutter.getLogger().debug("Player moving: " + moving + ", stamina full: " + (getCurrentStamina() >= getMaximumStamina()) + ", recalculate stamina"); int maxStamina = player.getProperty(PlayerProperty.PROP_MAX_STAMINA);
Consumption consumption = new Consumption(ConsumptionType.None); if (moving || (currentStamina < maxStamina)) {
Grasscutter.getLogger().trace("Player moving: " + moving + ", stamina full: " +
(currentStamina >= maxStamina) + ", recalculate stamina");
// TODO: refactor these conditions. Consumption consumption = new Consumption(ConsumptionType.None);
if (MotionStatesCategorized.get("CLIMB").contains(currentState)) { if (MotionStatesCategorized.get("CLIMB").contains(currentState)) {
consumption = getClimbConsumption(); consumption = getClimbSustainedConsumption();
} else if (MotionStatesCategorized.get("SWIM").contains((currentState))) { } else if (MotionStatesCategorized.get("SWIM").contains((currentState))) {
consumption = getSwimConsumptions(); consumption = getSwimSustainedConsumptions();
} else if (MotionStatesCategorized.get("RUN").contains(currentState)) { } else if (MotionStatesCategorized.get("RUN").contains(currentState)) {
consumption = getRunWalkDashConsumption(); consumption = getRunWalkDashSustainedConsumption();
} else if (MotionStatesCategorized.get("FLY").contains(currentState)) { } else if (MotionStatesCategorized.get("FLY").contains(currentState)) {
consumption = getFlyConsumption(); consumption = getFlySustainedConsumption();
} else if (MotionStatesCategorized.get("STANDBY").contains(currentState)) { } else if (MotionStatesCategorized.get("STANDBY").contains(currentState)) {
consumption = getStandConsumption(); consumption = getStandSustainedConsumption();
} else if (MotionStatesCategorized.get("FIGHT").contains(currentState)) {
consumption = getFightConsumption();
} }
// delay 2 seconds before start recovering - as official server does. /*
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 (cachedSession != null) {
if (consumption.amount < 0) { if (consumption.amount < 0) {
staminaRecoverDelay = 0; staminaRecoverDelay = 0;
} }
if (consumption.amount > 0 && consumption.consumptionType != ConsumptionType.POWERED_FLY) { if (consumption.amount > 0 && consumption.consumptionType != ConsumptionType.POWERED_FLY) {
// For POWERED_FLY recover immediately - things like Amber's gliding exam may require this.
if (staminaRecoverDelay < 10) { if (staminaRecoverDelay < 10) {
// For others recover after 2 seconds (10 ticks) - as official server does.
staminaRecoverDelay++; staminaRecoverDelay++;
consumption = new Consumption(ConsumptionType.None); consumption.amount = 0;
Grasscutter.getLogger().trace("[StaminaManager] Delaying recovery: " + staminaRecoverDelay);
} }
} }
// Grasscutter.getLogger().debug(getCurrentStamina() + "/" + getMaximumStamina() + "\t" + currentState + "\t" + "isMoving: " + isPlayerMoving() + "\t(" + consumption.consumptionType + "," + consumption.amount + ")"); updateStaminaRelative(cachedSession, consumption);
updateStamina(cachedSession, consumption.amount);
} }
// tick triggered
handleDrowning();
} }
} }
previousState = currentState; previousState = currentState;
previousCoordinates = new Position(currentCoordinates.getX(), previousCoordinates = new Position(
currentCoordinates.getY(), currentCoordinates.getZ());; currentCoordinates.getX(),
currentCoordinates.getY(),
currentCoordinates.getZ()
);
}
}
private void handleDrowning() {
int stamina = player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA);
if (stamina < 10) {
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);
}
} }
} }
private Consumption getClimbConsumption() { // 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); Consumption consumption = new Consumption(ConsumptionType.None);
if (currentState == MotionState.MOTION_CLIMB) { HashMap<Integer, Integer> fightingCost = new HashMap<>() {{
put(10013, -1000); // Kamisato Ayaka
put(10413, -1000); // Mona
}};
if (fightingCost.containsKey(skillCasting)) {
consumption = new Consumption(ConsumptionType.FIGHT, fightingCost.get(skillCasting));
}
return consumption;
}
private Consumption getClimbSustainedConsumption() {
Consumption consumption = new Consumption(ConsumptionType.None);
if (currentState == MotionState.MOTION_CLIMB && isPlayerMoving()) {
consumption = new Consumption(ConsumptionType.CLIMBING); consumption = new Consumption(ConsumptionType.CLIMBING);
if (previousState != MotionState.MOTION_CLIMB && previousState != MotionState.MOTION_CLIMB_JUMP) { if (previousState != MotionState.MOTION_CLIMB && previousState != MotionState.MOTION_CLIMB_JUMP) {
consumption = new Consumption(ConsumptionType.CLIMB_START); consumption = new Consumption(ConsumptionType.CLIMB_START);
} }
if (!isPlayerMoving()) {
consumption = new Consumption(ConsumptionType.None);
}
}
if (currentState == MotionState.MOTION_CLIMB_JUMP) {
if (previousState != MotionState.MOTION_CLIMB_JUMP) {
consumption = new Consumption(ConsumptionType.CLIMB_JUMP);
}
} }
// TODO: Foods
return consumption; return consumption;
} }
private Consumption getSwimConsumptions() { private Consumption getSwimSustainedConsumptions() {
handleDrowning();
Consumption consumption = new Consumption(ConsumptionType.None); Consumption consumption = new Consumption(ConsumptionType.None);
if (currentState == MotionState.MOTION_SWIM_MOVE) { if (currentState == MotionState.MOTION_SWIM_MOVE) {
consumption = new Consumption(ConsumptionType.SWIMMING); consumption = new Consumption(ConsumptionType.SWIMMING);
} }
if (currentState == MotionState.MOTION_SWIM_DASH) { if (currentState == MotionState.MOTION_SWIM_DASH) {
consumption = new Consumption(ConsumptionType.SWIM_DASH_START); consumption = new Consumption(ConsumptionType.SWIM_DASH);
if (previousState == MotionState.MOTION_SWIM_DASH) {
consumption = new Consumption(ConsumptionType.SWIM_DASH);
}
} }
return consumption; return consumption;
} }
private Consumption getRunWalkDashConsumption() { private Consumption getRunWalkDashSustainedConsumption() {
Consumption consumption = new Consumption(ConsumptionType.None); Consumption consumption = new Consumption(ConsumptionType.None);
if (currentState == MotionState.MOTION_DASH_BEFORE_SHAKE) {
consumption = new Consumption(ConsumptionType.DASH);
if (previousState == MotionState.MOTION_DASH_BEFORE_SHAKE) {
// only charge once
consumption = new Consumption(ConsumptionType.SPRINT);
}
}
if (currentState == MotionState.MOTION_DASH) { if (currentState == MotionState.MOTION_DASH) {
consumption = new Consumption(ConsumptionType.SPRINT); consumption = new Consumption(ConsumptionType.DASH);
// TODO: Foods
} }
if (currentState == MotionState.MOTION_RUN) { if (currentState == MotionState.MOTION_RUN) {
consumption = new Consumption(ConsumptionType.RUN); consumption = new Consumption(ConsumptionType.RUN);
...@@ -385,30 +432,34 @@ public class MovementManager { ...@@ -385,30 +432,34 @@ public class MovementManager {
return consumption; return consumption;
} }
private Consumption getFlyConsumption() { 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); Consumption consumption = new Consumption(ConsumptionType.FLY);
// Talent
HashMap<Integer, Float> glidingCostReduction = new HashMap<>() {{ HashMap<Integer, Float> glidingCostReduction = new HashMap<>() {{
put(212301, 0.8f); // Amber put(212301, 0.8f); // Amber
put(222301, 0.8f); // Venti put(222301, 0.8f); // Venti
}}; }};
float reduction = 1; float reduction = 1;
for (EntityAvatar entity: cachedSession.getPlayer().getTeamManager().getActiveTeam()) { for (EntityAvatar entity : cachedSession.getPlayer().getTeamManager().getActiveTeam()) {
for (int skillId: entity.getAvatar().getProudSkillList()) { for (int skillId : entity.getAvatar().getProudSkillList()) {
if (glidingCostReduction.containsKey(skillId)) { if (glidingCostReduction.containsKey(skillId)) {
reduction = glidingCostReduction.get(skillId); float potentialLowerReduction = glidingCostReduction.get(skillId);
if (potentialLowerReduction < reduction) {
reduction = potentialLowerReduction;
}
} }
} }
} }
consumption.amount *= reduction; consumption.amount *= reduction;
// TODO: Foods
// POWERED_FLY, e.g. wind tunnel
if (currentState == MotionState.MOTION_POWERED_FLY) {
consumption = new Consumption(ConsumptionType.POWERED_FLY);
}
return consumption; return consumption;
} }
private Consumption getStandConsumption() { private Consumption getStandSustainedConsumption() {
Consumption consumption = new Consumption(ConsumptionType.None); Consumption consumption = new Consumption(ConsumptionType.None);
if (currentState == MotionState.MOTION_STANDBY) { if (currentState == MotionState.MOTION_STANDBY) {
consumption = new Consumption(ConsumptionType.STANDBY); consumption = new Consumption(ConsumptionType.STANDBY);
...@@ -418,25 +469,4 @@ public class MovementManager { ...@@ -418,25 +469,4 @@ public class MovementManager {
} }
return consumption; return consumption;
} }
private Consumption getFightConsumption() {
Consumption consumption = new Consumption(ConsumptionType.None);
HashMap<Integer, Integer> fightingCost = new HashMap<>() {{
put(10013, -1000); // Kamisato Ayaka
put(10413, -1000); // Mona
}};
if (fightingCost.containsKey(skillCasting)) {
consumption = new Consumption(ConsumptionType.FIGHT, fightingCost.get(skillCasting));
// only handle once, so reset.
skillCasting = 0;
skillCaster = 0;
}
return consumption;
}
public void notifySkill(int caster, int skillId) {
skillCaster = caster;
skillCasting = skillId;
}
} }
...@@ -22,8 +22,8 @@ import emu.grasscutter.game.inventory.GameItem; ...@@ -22,8 +22,8 @@ import emu.grasscutter.game.inventory.GameItem;
import emu.grasscutter.game.inventory.Inventory; import emu.grasscutter.game.inventory.Inventory;
import emu.grasscutter.game.mail.Mail; import emu.grasscutter.game.mail.Mail;
import emu.grasscutter.game.mail.MailHandler; import emu.grasscutter.game.mail.MailHandler;
import emu.grasscutter.game.managers.MovementManager.MovementManager; import emu.grasscutter.game.managers.StaminaManager.StaminaManager;
import emu.grasscutter.game.managers.SotSManager.SotSManager; import emu.grasscutter.game.managers.SotSManager;
import emu.grasscutter.game.props.ActionReason; import emu.grasscutter.game.props.ActionReason;
import emu.grasscutter.game.props.EntityType; import emu.grasscutter.game.props.EntityType;
import emu.grasscutter.game.props.PlayerProperty; import emu.grasscutter.game.props.PlayerProperty;
...@@ -62,9 +62,6 @@ import java.util.concurrent.LinkedBlockingQueue; ...@@ -62,9 +62,6 @@ import java.util.concurrent.LinkedBlockingQueue;
@Entity(value = "players", useDiscriminator = false) @Entity(value = "players", useDiscriminator = false)
public class Player { public class Player {
@Transient private static int GlobalMaximumSpringVolume = 8500000;
@Transient private static int GlobalMaximumStamina = 24000;
@Id private int id; @Id private int id;
@Indexed(options = @IndexOptions(unique = true)) private String accountId; @Indexed(options = @IndexOptions(unique = true)) private String accountId;
...@@ -132,7 +129,7 @@ public class Player { ...@@ -132,7 +129,7 @@ public class Player {
@Transient private final InvokeHandler<AbilityInvokeEntry> clientAbilityInitFinishHandler; @Transient private final InvokeHandler<AbilityInvokeEntry> clientAbilityInitFinishHandler;
private MapMarksManager mapMarksManager; private MapMarksManager mapMarksManager;
@Transient private MovementManager movementManager; @Transient private StaminaManager staminaManager;
private long springLastUsed; private long springLastUsed;
...@@ -178,7 +175,7 @@ public class Player { ...@@ -178,7 +175,7 @@ public class Player {
this.expeditionInfo = new HashMap<>(); this.expeditionInfo = new HashMap<>();
this.messageHandler = null; this.messageHandler = null;
this.mapMarksManager = new MapMarksManager(); this.mapMarksManager = new MapMarksManager();
this.movementManager = new MovementManager(this); this.staminaManager = new StaminaManager(this);
this.sotsManager = new SotSManager(this); this.sotsManager = new SotSManager(this);
} }
...@@ -206,7 +203,7 @@ public class Player { ...@@ -206,7 +203,7 @@ public class Player {
this.getRotation().set(0, 307, 0); this.getRotation().set(0, 307, 0);
this.messageHandler = null; this.messageHandler = null;
this.mapMarksManager = new MapMarksManager(); this.mapMarksManager = new MapMarksManager();
this.movementManager = new MovementManager(this); this.staminaManager = new StaminaManager(this);
this.sotsManager = new SotSManager(this); this.sotsManager = new SotSManager(this);
} }
...@@ -875,11 +872,11 @@ public class Player { ...@@ -875,11 +872,11 @@ public class Player {
} }
public void onPause() { public void onPause() {
getStaminaManager().stopSustainedStaminaHandler();
} }
public void onUnpause() { public void onUnpause() {
getStaminaManager().startSustainedStaminaHandler();
} }
public void sendPacket(BasePacket packet) { public void sendPacket(BasePacket packet) {
...@@ -1024,7 +1021,7 @@ public class Player { ...@@ -1024,7 +1021,7 @@ public class Player {
return mapMarksManager; return mapMarksManager;
} }
public MovementManager getMovementManager() { return movementManager; } public StaminaManager getStaminaManager() { return staminaManager; }
public SotSManager getSotSManager() { return sotsManager; } public SotSManager getSotSManager() { return sotsManager; }
...@@ -1152,7 +1149,7 @@ public class Player { ...@@ -1152,7 +1149,7 @@ public class Player {
public void onLogout() { public void onLogout() {
// stop stamina calculation // stop stamina calculation
getMovementManager().resetTimer(); getStaminaManager().stopSustainedStaminaHandler();
// force to leave the dungeon // force to leave the dungeon
if (getScene().getSceneType() == SceneType.SCENE_DUNGEON) { if (getScene().getSceneType() == SceneType.SCENE_DUNGEON) {
...@@ -1214,7 +1211,7 @@ public class Player { ...@@ -1214,7 +1211,7 @@ public class Player {
} else if (prop == PlayerProperty.PROP_LAST_CHANGE_AVATAR_TIME) { // 10001 } else if (prop == PlayerProperty.PROP_LAST_CHANGE_AVATAR_TIME) { // 10001
// TODO: implement sanity check // TODO: implement sanity check
} else if (prop == PlayerProperty.PROP_MAX_SPRING_VOLUME) { // 10002 } else if (prop == PlayerProperty.PROP_MAX_SPRING_VOLUME) { // 10002
if (!(value >= 0 && value <= GlobalMaximumSpringVolume)) { return false; } if (!(value >= 0 && value <= getSotSManager().GlobalMaximumSpringVolume)) { return false; }
} else if (prop == PlayerProperty.PROP_CUR_SPRING_VOLUME) { // 10003 } else if (prop == PlayerProperty.PROP_CUR_SPRING_VOLUME) { // 10003
int playerMaximumSpringVolume = getProperty(PlayerProperty.PROP_MAX_SPRING_VOLUME); int playerMaximumSpringVolume = getProperty(PlayerProperty.PROP_MAX_SPRING_VOLUME);
if (!(value >= 0 && value <= playerMaximumSpringVolume)) { return false; } if (!(value >= 0 && value <= playerMaximumSpringVolume)) { return false; }
...@@ -1231,7 +1228,7 @@ public class Player { ...@@ -1231,7 +1228,7 @@ public class Player {
} else if (prop == PlayerProperty.PROP_IS_TRANSFERABLE) { // 10009 } else if (prop == PlayerProperty.PROP_IS_TRANSFERABLE) { // 10009
if (!(0 <= value && value <= 1)) { return false; } if (!(0 <= value && value <= 1)) { return false; }
} else if (prop == PlayerProperty.PROP_MAX_STAMINA) { // 10010 } else if (prop == PlayerProperty.PROP_MAX_STAMINA) { // 10010
if (!(value >= 0 && value <= GlobalMaximumStamina)) { return false; } if (!(value >= 0 && value <= getStaminaManager().GlobalMaximumStamina)) { return false; }
} else if (prop == PlayerProperty.PROP_CUR_PERSIST_STAMINA) { // 10011 } else if (prop == PlayerProperty.PROP_CUR_PERSIST_STAMINA) { // 10011
int playerMaximumStamina = getProperty(PlayerProperty.PROP_MAX_STAMINA); int playerMaximumStamina = getProperty(PlayerProperty.PROP_MAX_STAMINA);
if (!(value >= 0 && value <= playerMaximumStamina)) { return false; } if (!(value >= 0 && value <= playerMaximumStamina)) { return false; }
...@@ -1242,7 +1239,7 @@ public class Player { ...@@ -1242,7 +1239,7 @@ public class Player {
} else if (prop == PlayerProperty.PROP_PLAYER_EXP) { // 10014 } else if (prop == PlayerProperty.PROP_PLAYER_EXP) { // 10014
if (!(0 <= value)) { return false; } if (!(0 <= value)) { return false; }
} else if (prop == PlayerProperty.PROP_PLAYER_HCOIN) { // 10015 } else if (prop == PlayerProperty.PROP_PLAYER_HCOIN) { // 10015
// see 10015 // see PlayerProperty.PROP_PLAYER_HCOIN comments
} else if (prop == PlayerProperty.PROP_PLAYER_SCOIN) { // 10016 } else if (prop == PlayerProperty.PROP_PLAYER_SCOIN) { // 10016
// See 10015 // See 10015
} else if (prop == PlayerProperty.PROP_PLAYER_MP_SETTING_TYPE) { // 10017 } else if (prop == PlayerProperty.PROP_PLAYER_MP_SETTING_TYPE) { // 10017
......
...@@ -557,7 +557,7 @@ public class TeamManager { ...@@ -557,7 +557,7 @@ public class TeamManager {
// return; // return;
// } // }
// } // }
player.getMovementManager().resetTimer(); // prevent drowning immediately after respawn player.getStaminaManager().stopSustainedStaminaHandler(); // prevent drowning immediately after respawn
// Revive all team members // Revive all team members
for (EntityAvatar entity : getActiveTeam()) { for (EntityAvatar entity : getActiveTeam()) {
......
...@@ -4,12 +4,12 @@ import emu.grasscutter.Grasscutter; ...@@ -4,12 +4,12 @@ import emu.grasscutter.Grasscutter;
import emu.grasscutter.server.event.Event; import emu.grasscutter.server.event.Event;
import emu.grasscutter.server.event.EventHandler; import emu.grasscutter.server.event.EventHandler;
import emu.grasscutter.server.event.HandlerPriority; import emu.grasscutter.server.event.HandlerPriority;
import emu.grasscutter.utils.EventConsumer;
import emu.grasscutter.utils.Utils; import emu.grasscutter.utils.Utils;
import java.io.File; import java.io.File;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.net.URLClassLoader; import java.net.URLClassLoader;
import java.util.*; import java.util.*;
...@@ -47,12 +47,23 @@ public final class PluginManager { ...@@ -47,12 +47,23 @@ public final class PluginManager {
List<File> plugins = Arrays.stream(files) List<File> plugins = Arrays.stream(files)
.filter(file -> file.getName().endsWith(".jar")) .filter(file -> file.getName().endsWith(".jar"))
.toList(); .toList();
URL[] pluginNames = new URL[plugins.size()];
plugins.forEach(plugin -> {
try {
pluginNames[plugins.indexOf(plugin)] = plugin.toURI().toURL();
} catch (MalformedURLException exception) {
Grasscutter.getLogger().warn("Unable to load plugin.", exception);
}
});
URLClassLoader classLoader = new URLClassLoader(pluginNames);
plugins.forEach(plugin -> { plugins.forEach(plugin -> {
try { try {
URL url = plugin.toURI().toURL(); URL url = plugin.toURI().toURL();
try (URLClassLoader loader = new URLClassLoader(new URL[]{url})) { try (URLClassLoader loader = new URLClassLoader(new URL[]{url})) {
URL configFile = loader.findResource("plugin.json"); URL configFile = loader.findResource("plugin.json"); // Find the plugin.json file for each plugin.
InputStreamReader fileReader = new InputStreamReader(configFile.openStream()); InputStreamReader fileReader = new InputStreamReader(configFile.openStream());
PluginConfig pluginConfig = Grasscutter.getGsonFactory().fromJson(fileReader, PluginConfig.class); PluginConfig pluginConfig = Grasscutter.getGsonFactory().fromJson(fileReader, PluginConfig.class);
...@@ -68,10 +79,10 @@ public final class PluginManager { ...@@ -68,10 +79,10 @@ public final class PluginManager {
JarEntry entry = entries.nextElement(); JarEntry entry = entries.nextElement();
if(entry.isDirectory() || !entry.getName().endsWith(".class") || entry.getName().contains("module-info")) continue; if(entry.isDirectory() || !entry.getName().endsWith(".class") || entry.getName().contains("module-info")) continue;
String className = entry.getName().replace(".class", "").replace("/", "."); String className = entry.getName().replace(".class", "").replace("/", ".");
loader.loadClass(className); classLoader.loadClass(className); // Use the same class loader for ALL plugins.
} }
Class<?> pluginClass = loader.loadClass(pluginConfig.mainClass); Class<?> pluginClass = classLoader.loadClass(pluginConfig.mainClass);
Plugin pluginInstance = (Plugin) pluginClass.getDeclaredConstructor().newInstance(); Plugin pluginInstance = (Plugin) pluginClass.getDeclaredConstructor().newInstance();
this.loadPlugin(pluginInstance, PluginIdentifier.fromPluginConfig(pluginConfig), loader); this.loadPlugin(pluginInstance, PluginIdentifier.fromPluginConfig(pluginConfig), loader);
...@@ -156,6 +167,10 @@ public final class PluginManager { ...@@ -156,6 +167,10 @@ public final class PluginManager {
.toList().forEach(handler -> this.invokeHandler(event, handler)); .toList().forEach(handler -> this.invokeHandler(event, handler));
} }
public Plugin getPlugin(String name) {
return this.plugins.get(name);
}
/** /**
* Performs logic checks then invokes the provided event handler. * Performs logic checks then invokes the provided event handler.
* @param event The event passed through to the handler. * @param event The event passed through to the handler.
...@@ -167,4 +182,4 @@ public final class PluginManager { ...@@ -167,4 +182,4 @@ public final class PluginManager {
(event.isCanceled() && handler.ignoresCanceled()) (event.isCanceled() && handler.ignoresCanceled())
) handler.getCallback().consume((T) event); ) handler.getCallback().consume((T) event);
} }
} }
\ No newline at end of file
package emu.grasscutter.server.packet.recv; package emu.grasscutter.server.packet.recv;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.game.entity.GameEntity; import emu.grasscutter.game.entity.GameEntity;
import emu.grasscutter.game.props.FightProperty;
import emu.grasscutter.net.packet.Opcodes; import emu.grasscutter.net.packet.Opcodes;
import emu.grasscutter.net.packet.PacketOpcodes; import emu.grasscutter.net.packet.PacketOpcodes;
import emu.grasscutter.net.proto.CombatInvocationsNotifyOuterClass.CombatInvocationsNotify; import emu.grasscutter.net.proto.CombatInvocationsNotifyOuterClass.CombatInvocationsNotify;
...@@ -8,11 +10,19 @@ import emu.grasscutter.net.proto.CombatInvokeEntryOuterClass.CombatInvokeEntry; ...@@ -8,11 +10,19 @@ import emu.grasscutter.net.proto.CombatInvokeEntryOuterClass.CombatInvokeEntry;
import emu.grasscutter.net.proto.EntityMoveInfoOuterClass.EntityMoveInfo; import emu.grasscutter.net.proto.EntityMoveInfoOuterClass.EntityMoveInfo;
import emu.grasscutter.net.proto.EvtBeingHitInfoOuterClass.EvtBeingHitInfo; import emu.grasscutter.net.proto.EvtBeingHitInfoOuterClass.EvtBeingHitInfo;
import emu.grasscutter.net.packet.PacketHandler; import emu.grasscutter.net.packet.PacketHandler;
import emu.grasscutter.net.proto.MotionInfoOuterClass.MotionInfo;
import emu.grasscutter.net.proto.MotionStateOuterClass.MotionState;
import emu.grasscutter.net.proto.PlayerDieTypeOuterClass;
import emu.grasscutter.server.game.GameSession; import emu.grasscutter.server.game.GameSession;
import emu.grasscutter.server.packet.send.PacketEntityFightPropUpdateNotify;
@Opcodes(PacketOpcodes.CombatInvocationsNotify) @Opcodes(PacketOpcodes.CombatInvocationsNotify)
public class HandlerCombatInvocationsNotify extends PacketHandler { public class HandlerCombatInvocationsNotify extends PacketHandler {
private float cachedLandingSpeed = 0;
private long cachedLandingTimeMillisecond = 0;
private boolean monitorLandingEvent = false;
@Override @Override
public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
CombatInvocationsNotify notif = CombatInvocationsNotify.parseFrom(payload); CombatInvocationsNotify notif = CombatInvocationsNotify.parseFrom(payload);
...@@ -28,7 +38,34 @@ public class HandlerCombatInvocationsNotify extends PacketHandler { ...@@ -28,7 +38,34 @@ public class HandlerCombatInvocationsNotify extends PacketHandler {
EntityMoveInfo moveInfo = EntityMoveInfo.parseFrom(entry.getCombatData()); EntityMoveInfo moveInfo = EntityMoveInfo.parseFrom(entry.getCombatData());
GameEntity entity = session.getPlayer().getScene().getEntityById(moveInfo.getEntityId()); GameEntity entity = session.getPlayer().getScene().getEntityById(moveInfo.getEntityId());
if (entity != null) { if (entity != null) {
session.getPlayer().getMovementManager().handle(session, moveInfo, entity); // Move player
MotionInfo motionInfo = moveInfo.getMotionInfo();
entity.getPosition().set(motionInfo.getPos());
entity.getRotation().set(motionInfo.getRot());
entity.setLastMoveSceneTimeMs(moveInfo.getSceneTime());
entity.setLastMoveReliableSeq(moveInfo.getReliableSeq());
MotionState motionState = motionInfo.getState();
entity.setMotionState(motionState);
session.getPlayer().getStaminaManager().handleCombatInvocationsNotify(session, moveInfo, entity);
// 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);
}
}
} }
break; break;
default: default:
...@@ -47,5 +84,48 @@ public class HandlerCombatInvocationsNotify extends PacketHandler { ...@@ -47,5 +84,48 @@ public class HandlerCombatInvocationsNotify extends PacketHandler {
} }
} }
private void handleFallOnGround(GameSession session, GameEntity entity, MotionState motionState) {
// 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().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 damageFactor = 0;
if (cachedLandingSpeed < -23.5) {
damageFactor = 0.33f;
}
if (cachedLandingSpeed < -25) {
damageFactor = 0.5f;
}
if (cachedLandingSpeed < -26.5) {
damageFactor = 0.66f;
}
if (cachedLandingSpeed < -28) {
damageFactor = 1f;
}
float damage = maxHP * damageFactor;
float newHP = currentHP - damage;
if (newHP < 0) {
newHP = 0;
}
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) {
session.getPlayer().getStaminaManager().killAvatar(session, entity, PlayerDieTypeOuterClass.PlayerDieType.PLAYER_DIE_FALL);
}
cachedLandingSpeed = 0;
}
} }
package emu.grasscutter.server.packet.recv; package emu.grasscutter.server.packet.recv;
import emu.grasscutter.game.managers.SotSManager.SotSManager; import emu.grasscutter.game.managers.SotSManager;
import emu.grasscutter.game.player.Player; import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.props.FightProperty;
import emu.grasscutter.net.packet.Opcodes; import emu.grasscutter.net.packet.Opcodes;
import emu.grasscutter.net.packet.PacketHandler; import emu.grasscutter.net.packet.PacketHandler;
import emu.grasscutter.net.packet.PacketOpcodes; import emu.grasscutter.net.packet.PacketOpcodes;
import emu.grasscutter.net.proto.ChangeHpReasonOuterClass.ChangeHpReason;
import emu.grasscutter.net.proto.PropChangeReasonOuterClass.PropChangeReason;
import emu.grasscutter.server.game.GameSession; import emu.grasscutter.server.game.GameSession;
import emu.grasscutter.server.packet.send.PacketAvatarFightPropUpdateNotify;
import emu.grasscutter.server.packet.send.PacketAvatarLifeStateChangeNotify;
import emu.grasscutter.server.packet.send.PacketEntityFightPropChangeReasonNotify;
import emu.grasscutter.server.packet.send.PacketEntityFightPropUpdateNotify;
import java.util.List;
@Opcodes(PacketOpcodes.EnterTransPointRegionNotify) @Opcodes(PacketOpcodes.EnterTransPointRegionNotify)
public class HandlerEnterTransPointRegionNotify extends PacketHandler { public class HandlerEnterTransPointRegionNotify extends PacketHandler {
......
package emu.grasscutter.server.packet.recv; package emu.grasscutter.server.packet.recv;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.net.packet.Opcodes; import emu.grasscutter.net.packet.Opcodes;
import emu.grasscutter.net.packet.PacketHandler; import emu.grasscutter.net.packet.PacketHandler;
import emu.grasscutter.net.packet.PacketOpcodes; import emu.grasscutter.net.packet.PacketOpcodes;
...@@ -15,10 +14,7 @@ public class HandlerEvtDoSkillSuccNotify extends PacketHandler { ...@@ -15,10 +14,7 @@ public class HandlerEvtDoSkillSuccNotify extends PacketHandler {
EvtDoSkillSuccNotify notify = EvtDoSkillSuccNotify.parseFrom(payload); EvtDoSkillSuccNotify notify = EvtDoSkillSuccNotify.parseFrom(payload);
// TODO: Will be used for deducting stamina for charged skills. // TODO: Will be used for deducting stamina for charged skills.
int caster = notify.getCasterId(); session.getPlayer().getStaminaManager().handleEvtDoSkillSuccNotify(session, notify);
int skillId = notify.getSkillId();
session.getPlayer().getMovementManager().notifySkill(caster, skillId);
} }
} }
package emu.grasscutter.server.packet.recv; package emu.grasscutter.server.packet.recv;
import emu.grasscutter.game.managers.SotSManager.SotSManager; import emu.grasscutter.game.managers.SotSManager;
import emu.grasscutter.game.player.Player; import emu.grasscutter.game.player.Player;
import emu.grasscutter.net.packet.Opcodes; import emu.grasscutter.net.packet.Opcodes;
import emu.grasscutter.net.packet.PacketHandler; import emu.grasscutter.net.packet.PacketHandler;
......
...@@ -13,16 +13,16 @@ import java.io.PrintWriter; ...@@ -13,16 +13,16 @@ import java.io.PrintWriter;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.ArrayList; import java.util.*;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
import emu.grasscutter.GameConstants; import emu.grasscutter.GameConstants;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
import emu.grasscutter.command.Command;
import emu.grasscutter.command.CommandHandler;
import emu.grasscutter.command.CommandMap;
import emu.grasscutter.data.GameData; import emu.grasscutter.data.GameData;
import emu.grasscutter.data.ResourceLoader; import emu.grasscutter.data.ResourceLoader;
import emu.grasscutter.data.def.AvatarData; import emu.grasscutter.data.def.AvatarData;
...@@ -31,6 +31,8 @@ import emu.grasscutter.data.def.MonsterData; ...@@ -31,6 +31,8 @@ import emu.grasscutter.data.def.MonsterData;
import emu.grasscutter.data.def.SceneData; import emu.grasscutter.data.def.SceneData;
import emu.grasscutter.utils.Utils; import emu.grasscutter.utils.Utils;
import static emu.grasscutter.utils.Language.translate;
public final class Tools { public final class Tools {
public static void createGmHandbook() throws Exception { public static void createGmHandbook() throws Exception {
ToolsWithLanguageOption.createGmHandbook(getLanguageOption()); ToolsWithLanguageOption.createGmHandbook(getLanguageOption());
...@@ -111,7 +113,20 @@ final class ToolsWithLanguageOption { ...@@ -111,7 +113,20 @@ final class ToolsWithLanguageOption {
writer.println("// Grasscutter " + GameConstants.VERSION + " GM Handbook"); writer.println("// Grasscutter " + GameConstants.VERSION + " GM Handbook");
writer.println("// Created " + dtf.format(now) + System.lineSeparator() + System.lineSeparator()); writer.println("// Created " + dtf.format(now) + System.lineSeparator() + System.lineSeparator());
CommandMap cmdMap = new CommandMap(true);
List<Command> cmdList = new ArrayList<>(cmdMap.getAnnotationsAsList());
writer.println("// Commands");
for (Command cmd : cmdList) {
String cmdName = cmd.label();
while (cmdName.length() <= 15) {
cmdName = " " + cmdName;
}
writer.println(cmdName + " : " + translate(cmd.description()));
}
writer.println();
list = new ArrayList<>(GameData.getAvatarDataMap().keySet()); list = new ArrayList<>(GameData.getAvatarDataMap().keySet());
Collections.sort(list); Collections.sort(list);
......
...@@ -19,7 +19,7 @@ public final class Language { ...@@ -19,7 +19,7 @@ public final class Language {
* @return A language instance. * @return A language instance.
*/ */
public static Language getLanguage(String langCode) { public static Language getLanguage(String langCode) {
return new Language(langCode + ".json"); return new Language(langCode + ".json", Grasscutter.getConfig().DefaultLanguage.toLanguageTag());
} }
/** /**
...@@ -30,6 +30,7 @@ public final class Language { ...@@ -30,6 +30,7 @@ public final class Language {
*/ */
public static String translate(String key, Object... args) { public static String translate(String key, Object... args) {
String translated = Grasscutter.getLanguage().get(key); String translated = Grasscutter.getLanguage().get(key);
try { try {
return translated.formatted(args); return translated.formatted(args);
} catch (Exception exception) { } catch (Exception exception) {
...@@ -38,48 +39,27 @@ public final class Language { ...@@ -38,48 +39,27 @@ public final class Language {
} }
} }
/**
* creates a language instance.
* @param fileName The name of the language file.
*/
private Language(String fileName) {
@Nullable JsonObject languageData = null;
languageData = loadLanguage(fileName);
if (languageData == null) {
Grasscutter.getLogger().info("Now switch to default language");
languageData = loadDefaultLanguage();
}
assert languageData != null : "languageData is null";
this.languageData = languageData;
}
/**
* Load default language file and creates a language instance.
* @return language data
*/
private JsonObject loadDefaultLanguage() {
var fileName = Grasscutter.getConfig().DefaultLanguage.toLanguageTag() + ".json";
return loadLanguage(fileName);
}
/** /**
* Reads a file and creates a language instance. * Reads a file and creates a language instance.
* @param fileName The name of the language file. * @param fileName The name of the language file.
* @return language data
*/ */
private JsonObject loadLanguage(String fileName) { private Language(String fileName, String fallback) {
@Nullable JsonObject languageData = null; @Nullable JsonObject languageData = null;
try { try {
InputStream file = Grasscutter.class.getResourceAsStream("/languages/" + fileName); InputStream file = Grasscutter.class.getResourceAsStream("/languages/" + fileName);
languageData = Grasscutter.getGsonFactory().fromJson(Utils.readFromInputStream(file), JsonObject.class); String translationContents = Utils.readFromInputStream(file);
if(translationContents.equals("empty")) {
file = Grasscutter.class.getResourceAsStream("/languages/" + fallback);
translationContents = Utils.readFromInputStream(file);
}
languageData = Grasscutter.getGsonFactory().fromJson(translationContents, JsonObject.class);
} catch (Exception exception) { } catch (Exception exception) {
Grasscutter.getLogger().warn("Failed to load language file: " + fileName); Grasscutter.getLogger().warn("Failed to load language file: " + fileName, exception);
} }
return languageData;
this.languageData = languageData;
} }
/** /**
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment