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
...@@ -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