Skip to content
Snippets Groups Projects
Commit d468edcf authored by Akka's avatar Akka
Browse files

merge

parents 4b6842f0 3a5503de
No related merge requests found
Showing
with 933 additions and 127 deletions
...@@ -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 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(-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;
}
}
\ 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,81 +7,43 @@ import emu.grasscutter.game.player.Player; ...@@ -7,81 +7,43 @@ 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 final Timer sustainedStaminaHandlerTimer = new Timer();
private final SustainedStaminaHandler handleSustainedStamina = new SustainedStaminaHandler();
private boolean timerRunning = false;
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 boolean isInSkillMove = false;
private int skillCasting = 0; public boolean getIsInSkillMove() {
return isInSkillMove;
}
public void setIsInSkillMove(boolean b) {
isInSkillMove = b;
}
public MovementManager(Player player) { public StaminaManager(Player player) {
previousCoordinates.add(new Position(0,0,0));
this.player = player; this.player = player;
MotionStatesCategorized.put("SWIM", new HashSet<>(Arrays.asList( MotionStatesCategorized.put("SWIM", new HashSet<>(Arrays.asList(
...@@ -129,252 +91,227 @@ public class MovementManager { ...@@ -129,252 +91,227 @@ public class MovementManager {
))); )));
MotionStatesCategorized.put("FIGHT", new HashSet<>(Arrays.asList( MotionStatesCategorized.put("FIGHT", new HashSet<>(Arrays.asList(
MotionState.MOTION_FIGHT MotionState.MOTION_FIGHT
))); )));
}
public void handle(GameSession session, EntityMoveInfoOuterClass.EntityMoveInfo moveInfo, GameEntity entity) {
if (movementManagerTickTimer == null) {
movementManagerTickTimer = new Timer();
movementManagerTickTimer.scheduleAtFixedRate(new MotionManagerTick(), 0, 200);
}
// cache info for later use in tick
cachedSession = session;
cachedEntity = entity;
MotionInfo motionInfo = moveInfo.getMotionInfo();
moveEntity(entity, moveInfo);
VectorOuterClass.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;
}
currentState = motionInfo.getState();
Grasscutter.getLogger().debug("" + currentState + "\t" + (moveInfo.getIsReliable() ? "reliable" : ""));
handleFallOnGround(motionInfo);
}
public void resetTimer() {
Grasscutter.getLogger().debug("MovementManager ticker stopped");
movementManagerTickTimer.cancel();
movementManagerTickTimer = null;
}
private void moveEntity(GameEntity entity, EntityMoveInfoOuterClass.EntityMoveInfo moveInfo) {
entity.getPosition().set(moveInfo.getMotionInfo().getPos());
entity.getRotation().set(moveInfo.getMotionInfo().getRot());
entity.setLastMoveSceneTimeMs(moveInfo.getSceneTime());
entity.setLastMoveReliableSeq(moveInfo.getReliableSeq());
entity.setMotionState(moveInfo.getMotionInfo().getState());
} }
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().debug("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() {
return player.getProperty(PlayerProperty.PROP_MAX_STAMINA);
} }
// Returns new stamina // Returns new stamina and sends PlayerPropNotify
public int updateStamina(GameSession session, int amount) { public int updateStamina(GameSession session, Consumption consumption) {
int currentStamina = session.getPlayer().getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA); int currentStamina = player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA);
if (amount == 0) { if (consumption.amount == 0) {
return currentStamina; return currentStamina;
} }
int playerMaxStamina = session.getPlayer().getProperty(PlayerProperty.PROP_MAX_STAMINA); int playerMaxStamina = player.getProperty(PlayerProperty.PROP_MAX_STAMINA);
int newStamina = currentStamina + amount; Grasscutter.getLogger().debug(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;
} }
if (newStamina > playerMaxStamina) { if (newStamina > playerMaxStamina) {
newStamina = playerMaxStamina; newStamina = playerMaxStamina;
} }
session.getPlayer().setProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA, newStamina); 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));
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() && !timerRunning) {
timerRunning = true;
sustainedStaminaHandlerTimer.scheduleAtFixedRate(handleSustainedStamina, 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 (timerRunning) {
if ((System.currentTimeMillis() - landTimeMillisecond) > maxDelay) { timerRunning = false;
Grasscutter.getLogger().debug("MOTION_FALL_ON_GROUND received after " + maxDelay + "ms, discard."); sustainedStaminaHandlerTimer.cancel();
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, updateStamina(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)); updateStamina(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) {
updateStamina(session, new Consumption(ConsumptionType.SWIM_DASH_START));
}
break;
}
}
private void handleImmediateStamina(GameSession session, EvtDoSkillSuccNotify notify) {
Consumption consumption = getFightConsumption(notify.getSkillId());
updateStamina(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);
if (moving || (currentStamina < maxStamina)) {
Grasscutter.getLogger().debug("Player moving: " + moving + ", stamina full: " +
(currentStamina >= maxStamina) + ", recalculate stamina");
Consumption consumption = new Consumption(ConsumptionType.None); Consumption consumption = new Consumption(ConsumptionType.None);
if (!isInSkillMove) {
// TODO: refactor these conditions. if (MotionStatesCategorized.get("CLIMB").contains(currentState)) {
if (MotionStatesCategorized.get("CLIMB").contains(currentState)) { consumption = getClimbSustainedConsumption();
consumption = getClimbConsumption(); } else if (MotionStatesCategorized.get("SWIM").contains((currentState))) {
} else if (MotionStatesCategorized.get("SWIM").contains((currentState))) { consumption = getSwimSustainedConsumptions();
consumption = getSwimConsumptions(); } else if (MotionStatesCategorized.get("RUN").contains(currentState)) {
} else if (MotionStatesCategorized.get("RUN").contains(currentState)) { consumption = getRunWalkDashSustainedConsumption();
consumption = getRunWalkDashConsumption(); } else if (MotionStatesCategorized.get("FLY").contains(currentState)) {
} else if (MotionStatesCategorized.get("FLY").contains(currentState)) { consumption = getFlySustainedConsumption();
consumption = getFlyConsumption(); } else if (MotionStatesCategorized.get("STANDBY").contains(currentState)) {
} else if (MotionStatesCategorized.get("STANDBY").contains(currentState)) { consumption = getStandSustainedConsumption();
consumption = getStandConsumption(); }
} else if (MotionStatesCategorized.get("FIGHT").contains(currentState)) {
consumption = getFightConsumption();
} }
// delay 2 seconds before start recovering - as official server does.
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 = new Consumption(ConsumptionType.None);
} }
} }
// Grasscutter.getLogger().debug(getCurrentStamina() + "/" + getMaximumStamina() + "\t" + currentState + "\t" + "isMoving: " + isPlayerMoving() + "\t(" + consumption.consumptionType + "," + consumption.amount + ")"); updateStamina(cachedSession, consumption);
updateStamina(cachedSession, consumption.amount);
} }
// tick triggered
handleDrowning(); 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) {
boolean isSwimming = MotionStatesCategorized.get("SWIM").contains(currentState);
Grasscutter.getLogger().debug(player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA) + "/" +
player.getProperty(PlayerProperty.PROP_MAX_STAMINA) + "\t" + currentState + "\t" + isSwimming);
if (isSwimming && currentState != MotionState.MOTION_SWIM_IDLE) {
killAvatar(cachedSession, cachedEntity, PlayerDieType.PLAYER_DIE_DRAWN);
}
}
}
// Consumption Calculators
private Consumption getFightConsumption(int skillCasting) {
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));
} }
return consumption;
} }
private Consumption getClimbConsumption() { private Consumption getClimbSustainedConsumption() {
Consumption consumption = new Consumption(ConsumptionType.None); Consumption consumption = new Consumption(ConsumptionType.None);
if (currentState == MotionState.MOTION_CLIMB) { 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);
}
} }
return consumption; return consumption;
} }
private Consumption getSwimConsumptions() { private Consumption getSwimSustainedConsumptions() {
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);
} }
if (currentState == MotionState.MOTION_RUN) { if (currentState == MotionState.MOTION_RUN) {
consumption = new Consumption(ConsumptionType.RUN); consumption = new Consumption(ConsumptionType.RUN);
...@@ -385,22 +322,21 @@ public class MovementManager { ...@@ -385,22 +322,21 @@ public class MovementManager {
return consumption; return consumption;
} }
private Consumption getFlyConsumption() { private Consumption getFlySustainedConsumption() {
Consumption consumption = new Consumption(ConsumptionType.FLY); Consumption consumption = new Consumption(ConsumptionType.FLY);
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); reduction = glidingCostReduction.get(skillId);
} }
} }
} }
consumption.amount *= reduction; consumption.amount *= reduction;
// POWERED_FLY, e.g. wind tunnel // POWERED_FLY, e.g. wind tunnel
if (currentState == MotionState.MOTION_POWERED_FLY) { if (currentState == MotionState.MOTION_POWERED_FLY) {
consumption = new Consumption(ConsumptionType.POWERED_FLY); consumption = new Consumption(ConsumptionType.POWERED_FLY);
...@@ -408,7 +344,7 @@ public class MovementManager { ...@@ -408,7 +344,7 @@ public class MovementManager {
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 +354,4 @@ public class MovementManager { ...@@ -418,25 +354,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,33 @@ public class HandlerCombatInvocationsNotify extends PacketHandler { ...@@ -28,7 +38,33 @@ 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
// For plunge attacks, LAND_SPEED is always -30 and is not useful.
// May need the height when starting plunge attack.
if (monitorLandingEvent) {
if (motionState == MotionState.MOTION_FALL_ON_GROUND) {
monitorLandingEvent = false;
handleFallOnGround(session, entity, motionState);
}
}
if (motionState == MotionState.MOTION_LAND_SPEED) {
// MOTION_LAND_SPEED and MOTION_FALL_ON_GROUND arrive in different packet. Cache land speed for later use.
cachedLandingSpeed = motionInfo.getSpeed().getY();
cachedLandingTimeMillisecond = System.currentTimeMillis();
monitorLandingEvent = true;
}
} }
break; break;
default: default:
...@@ -47,5 +83,39 @@ public class HandlerCombatInvocationsNotify extends PacketHandler { ...@@ -47,5 +83,39 @@ public class HandlerCombatInvocationsNotify extends PacketHandler {
} }
} }
private void handleFallOnGround(GameSession session, GameEntity entity, MotionState motionState) {
// If not received immediately after MOTION_LAND_SPEED, discard this packet.
int maxDelay = 200;
long actualDelay = System.currentTimeMillis() - cachedLandingTimeMillisecond;
Grasscutter.getLogger().debug("MOTION_FALL_ON_GROUND received after " + actualDelay + "/" + maxDelay + "ms." + (actualDelay > maxDelay ? " Discard" : ""));
if (actualDelay > maxDelay) {
return;
}
float currentHP = entity.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP);
float maxHP = entity.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP);
float damage = 0;
if (cachedLandingSpeed < -23.5) {
damage = (float) (maxHP * 0.33);
}
if (cachedLandingSpeed < -25) {
damage = (float) (maxHP * 0.5);
}
if (cachedLandingSpeed < -26.5) {
damage = (float) (maxHP * 0.66);
}
if (cachedLandingSpeed < -28) {
damage = (maxHP * 1);
}
float newHP = currentHP - damage;
if (newHP < 0) {
newHP = 0;
}
Grasscutter.getLogger().debug(currentHP + "/" + maxHP + "\t" + "\tDamage: " + damage + "\tnewHP: " + newHP);
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;
} }
/** /**
......
package emu.grasscutter.utils; package emu.grasscutter.utils;
import java.io.*; import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.StandardCopyOption; import java.nio.file.StandardCopyOption;
import java.time.*; import java.time.*;
...@@ -17,6 +18,8 @@ import io.netty.buffer.Unpooled; ...@@ -17,6 +18,8 @@ import io.netty.buffer.Unpooled;
import org.slf4j.Logger; import org.slf4j.Logger;
import javax.annotation.Nullable;
import static emu.grasscutter.utils.Language.translate; import static emu.grasscutter.utils.Language.translate;
@SuppressWarnings({"UnusedReturnValue", "BooleanMethodIsAlwaysInverted"}) @SuppressWarnings({"UnusedReturnValue", "BooleanMethodIsAlwaysInverted"})
...@@ -252,14 +255,18 @@ public final class Utils { ...@@ -252,14 +255,18 @@ public final class Utils {
* @param stream The input stream. * @param stream The input stream.
* @return The string. * @return The string.
*/ */
public static String readFromInputStream(InputStream stream) { public static String readFromInputStream(@Nullable InputStream stream) {
if(stream == null) return "empty";
StringBuilder stringBuilder = new StringBuilder(); StringBuilder stringBuilder = new StringBuilder();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream,"UTF-8"))) { try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))) {
String line; while ((line = reader.readLine()) != null) { String line; while ((line = reader.readLine()) != null) {
stringBuilder.append(line); stringBuilder.append(line);
} stream.close(); } stream.close();
} catch (IOException e) { } catch (IOException e) {
Grasscutter.getLogger().warn("Failed to read from input stream."); Grasscutter.getLogger().warn("Failed to read from input stream.");
} catch (NullPointerException ignored) {
return "empty";
} return stringBuilder.toString(); } return stringBuilder.toString();
} }
......
...@@ -56,6 +56,7 @@ ...@@ -56,6 +56,7 @@
"console_execute_error": "This command can only be run from the console.", "console_execute_error": "This command can only be run from the console.",
"player_execute_error": "Run this command in-game.", "player_execute_error": "Run this command in-game.",
"command_exist_error": "No command found.", "command_exist_error": "No command found.",
"no_description_specified": "No description specified",
"invalid": { "invalid": {
"amount": "Invalid amount.", "amount": "Invalid amount.",
"artifactId": "Invalid artifactId.", "artifactId": "Invalid artifactId.",
...@@ -95,17 +96,20 @@ ...@@ -95,17 +96,20 @@
"create": "Account created with UID %s.", "create": "Account created with UID %s.",
"delete": "Account deleted.", "delete": "Account deleted.",
"no_account": "Account not found.", "no_account": "Account not found.",
"command_usage": "Usage: account <create|delete> <username> [uid]" "command_usage": "Usage: account <create|delete> <username> [uid]",
"description": "Modify user accounts"
}, },
"broadcast": { "broadcast": {
"command_usage": "Usage: broadcast <message>", "command_usage": "Usage: broadcast <message>",
"message_sent": "Message sent." "message_sent": "Message sent.",
"description": "Sends a message to all the players"
}, },
"changescene": { "changescene": {
"usage": "Usage: changescene <sceneId>", "usage": "Usage: changescene <sceneId>",
"already_in_scene": "You are already in that scene.", "already_in_scene": "You are already in that scene.",
"success": "Changed to scene %s.", "success": "Changed to scene %s.",
"exists_error": "The specified scene does not exist." "exists_error": "The specified scene does not exist.",
"description": "Changes your scene"
}, },
"clear": { "clear": {
"command_usage": "Usage: clear <all|wp|art|mat>", "command_usage": "Usage: clear <all|wp|art|mat>",
...@@ -115,35 +119,41 @@ ...@@ -115,35 +119,41 @@
"furniture": "Cleared furniture for %s.", "furniture": "Cleared furniture for %s.",
"displays": "Cleared displays for %s.", "displays": "Cleared displays for %s.",
"virtuals": "Cleared virtuals for %s.", "virtuals": "Cleared virtuals for %s.",
"everything": "Cleared everything for %s." "everything": "Cleared everything for %s.",
"description": "Deletes unequipped unlocked items, including yellow rarity ones from your inventory"
}, },
"coop": { "coop": {
"usage": "Usage: coop <playerId> <target playerId>", "usage": "Usage: coop <playerId> <target playerId>",
"success": "Summoned %s to %s's world." "success": "Summoned %s to %s's world.",
"description": "Forces someone to join the world of others"
}, },
"enter_dungeon": { "enter_dungeon": {
"usage": "Usage: enterdungeon <dungeon id>", "usage": "Usage: enterdungeon <dungeon id>",
"changed": "Changed to dungeon %s", "changed": "Changed to dungeon %s",
"not_found_error": "Dungeon does not exist", "not_found_error": "Dungeon does not exist",
"in_dungeon_error": "You are already in that dungeon" "in_dungeon_error": "You are already in that dungeon",
"description": "Enter a dungeon"
}, },
"giveAll": { "giveAll": {
"usage": "Usage: giveall [player] [amount]", "usage": "Usage: giveall [player] [amount]",
"started": "Receiving all items...", "started": "Receiving all items...",
"success": "Successfully gave all items to %s.", "success": "Successfully gave all items to %s.",
"invalid_amount_or_playerId": "Invalid amount or player ID." "invalid_amount_or_playerId": "Invalid amount or player ID.",
"description": "Gives all items"
}, },
"giveArtifact": { "giveArtifact": {
"usage": "Usage: giveart|gart [player] <artifactId> <mainPropId> [<appendPropId>[,<times>]]... [level]", "usage": "Usage: giveart|gart [player] <artifactId> <mainPropId> [<appendPropId>[,<times>]]... [level]",
"id_error": "Invalid artifact ID.", "id_error": "Invalid artifact ID.",
"success": "Given %s to %s." "success": "Given %s to %s.",
"description": "Gives the player a specified artifact"
}, },
"giveChar": { "giveChar": {
"usage": "Usage: givechar <player> <itemId|itemName> [amount]", "usage": "Usage: givechar <player> <itemId|itemName> [amount]",
"given": "Given %s with level %s to %s.", "given": "Given %s with level %s to %s.",
"invalid_avatar_id": "Invalid avatar id.", "invalid_avatar_id": "Invalid avatar id.",
"invalid_avatar_level": "Invalid avatar level.", "invalid_avatar_level": "Invalid avatar level.",
"invalid_avatar_or_player_id": "Invalid avatar or player ID." "invalid_avatar_or_player_id": "Invalid avatar or player ID.",
"description": "Gives the player a specified character"
}, },
"give": { "give": {
"usage": "Usage: give <player> <itemId|itemName> [amount] [level]", "usage": "Usage: give <player> <itemId|itemName> [amount] [level]",
...@@ -151,29 +161,36 @@ ...@@ -151,29 +161,36 @@
"refinement_must_between_1_and_5": "Refinement must be between 1 and 5.", "refinement_must_between_1_and_5": "Refinement must be between 1 and 5.",
"given": "Given %s of %s to %s.", "given": "Given %s of %s to %s.",
"given_with_level_and_refinement": "Given %s with level %s, refinement %s %s times to %s", "given_with_level_and_refinement": "Given %s with level %s, refinement %s %s times to %s",
"given_level": "Given %s with level %s %s times to %s" "given_level": "Given %s with level %s %s times to %s",
"description": "Gives an item to you or the specified player"
}, },
"godmode": { "godmode": {
"success": "Godmode is now %s for %s." "success": "Godmode is now %s for %s.",
"description": "Prevents you from taking damage. Defaults to toggle."
}, },
"heal": { "heal": {
"success": "All characters have been healed." "success": "All characters have been healed.",
"description": "Heal all characters in your current team."
}, },
"kick": { "kick": {
"player_kick_player": "Player [%s:%s] has kicked player [%s:%s]", "player_kick_player": "Player [%s:%s] has kicked player [%s:%s]",
"server_kick_player": "Kicking player [%s:%s]" "server_kick_player": "Kicking player [%s:%s]",
"description": "Kicks the specified player from the server (WIP)"
}, },
"kill": { "kill": {
"usage": "Usage: killall [playerUid] [sceneId]", "usage": "Usage: killall [playerUid] [sceneId]",
"scene_not_found_in_player_world": "Scene not found in player world", "scene_not_found_in_player_world": "Scene not found in player world",
"kill_monsters_in_scene": "Killing %s monsters in scene %s" "kill_monsters_in_scene": "Killing %s monsters in scene %s",
"description": "Kill all entities"
}, },
"killCharacter": { "killCharacter": {
"usage": "Usage: /killcharacter [playerId]", "usage": "Usage: /killcharacter [playerId]",
"success": "Killed %s's current character." "success": "Killed %s's current character.",
"description": "Kills the players current character"
}, },
"list": { "list": {
"success": "There are %s player(s) online:" "success": "There are %s player(s) online:",
"description": "List online players"
}, },
"permission": { "permission": {
"usage": "Usage: permission <add|remove> <username> <permission>", "usage": "Usage: permission <add|remove> <username> <permission>",
...@@ -181,21 +198,26 @@ ...@@ -181,21 +198,26 @@
"has_error": "They already have this permission!", "has_error": "They already have this permission!",
"remove": "Permission removed.", "remove": "Permission removed.",
"not_have_error": "They don't have this permission!", "not_have_error": "They don't have this permission!",
"account_error": "The account cannot be found." "account_error": "The account cannot be found.",
"description": "Grants or removes a permission for a user"
}, },
"position": { "position": {
"success": "Coordinates: %.3f, %.3f, %.3f\nScene id: %d" "success": "Coordinates: %s, %s, %s\nScene id: %s",
"description": "Get coordinates."
}, },
"reload": { "reload": {
"reload_start": "Reloading config.", "reload_start": "Reloading config.",
"reload_done": "Reload complete." "reload_done": "Reload complete.",
"description": "Reload server config"
}, },
"resetConst": { "resetConst": {
"reset_all": "Reset all avatars' constellations.", "reset_all": "Reset all avatars' constellations.",
"success": "Constellations for %s have been reset. Please relog to see changes." "success": "Constellations for %s have been reset. Please relog to see changes.",
"description": "Resets the constellation level on your current active character, will need to relog after using the command to see any changes."
}, },
"resetShopLimit": { "resetShopLimit": {
"usage": "Usage: /resetshop <player id>" "usage": "Usage: /resetshop <player id>",
"description": "Reset target player's shop refresh time."
}, },
"sendMail": { "sendMail": {
"usage": "Usage: give [player] <itemId|itemName> [amount]", "usage": "Usage: give [player] <itemId|itemName> [amount]",
...@@ -217,17 +239,20 @@ ...@@ -217,17 +239,20 @@
"message": "<message>", "message": "<message>",
"sender": "<sender>", "sender": "<sender>",
"arguments": "<itemId|itemName|finish> [amount] [level]", "arguments": "<itemId|itemName|finish> [amount] [level]",
"error": "ERROR: invalid construction stage %s. Check console for stacktrace." "error": "ERROR: invalid construction stage %s. Check console for stacktrace.",
"description": "Sends mail to the specified user. The usage of this command changes based on it's composition state."
}, },
"sendMessage": { "sendMessage": {
"usage": "Usage: sendmessage <player> <message>", "usage": "Usage: sendmessage <player> <message>",
"success": "Message sent." "success": "Message sent.",
"description": "Sends a message to a player as the server"
}, },
"setFetterLevel": { "setFetterLevel": {
"usage": "Usage: setfetterlevel <level>", "usage": "Usage: setfetterlevel <level>",
"range_error": "Fetter level must be between 0 and 10.", "range_error": "Fetter level must be between 0 and 10.",
"success": "Fetter level set to %s", "success": "Fetter level set to %s",
"level_error": "Invalid fetter level." "level_error": "Invalid fetter level.",
"description": "Sets your fetter level for your current active character"
}, },
"setStats": { "setStats": {
"usage_console": "Usage: setstats|stats @<UID> <stat> <value>", "usage_console": "Usage: setstats|stats @<UID> <stat> <value>",
...@@ -238,20 +263,24 @@ ...@@ -238,20 +263,24 @@
"player_error": "Player not found or offline.", "player_error": "Player not found or offline.",
"set_self": "%s set to %s.", "set_self": "%s set to %s.",
"set_for_uid": "%s for %s set to %s.", "set_for_uid": "%s for %s set to %s.",
"set_max_hp": "MAX HP set to %s." "set_max_hp": "MAX HP set to %s.",
"description": "Set fight property for your current active character"
}, },
"setWorldLevel": { "setWorldLevel": {
"usage": "Usage: setworldlevel <level>", "usage": "Usage: setworldlevel <level>",
"value_error": "World level must be between 0-8", "value_error": "World level must be between 0-8",
"success": "World level set to %s.", "success": "World level set to %s.",
"invalid_world_level": "Invalid world level." "invalid_world_level": "Invalid world level.",
"description": "Sets your world level (Relog to see proper effects)"
}, },
"spawn": { "spawn": {
"usage": "Usage: spawn <entityId> [amount] [level(monster only)]", "usage": "Usage: spawn <entityId> [amount] [level(monster only)]",
"success": "Spawned %s of %s." "success": "Spawned %s of %s.",
"description": "Spawns an entity near you"
}, },
"stop": { "stop": {
"success": "Server shutting down..." "success": "Server shutting down...",
"description": "Stops the server"
}, },
"talent": { "talent": {
"usage_1": "To set talent level: /talent set <talentID> <value>", "usage_1": "To set talent level: /talent set <talentID> <value>",
...@@ -267,18 +296,21 @@ ...@@ -267,18 +296,21 @@
"invalid_level": "Invalid talent level.", "invalid_level": "Invalid talent level.",
"normal_attack_id": "Normal Attack ID %s.", "normal_attack_id": "Normal Attack ID %s.",
"e_skill_id": "E skill ID %s.", "e_skill_id": "E skill ID %s.",
"q_skill_id": "Q skill ID %s." "q_skill_id": "Q skill ID %s.",
"description": "Set talent level for your current active character"
}, },
"teleportAll": { "teleportAll": {
"success": "Summoned all players to your location.", "success": "Summoned all players to your location.",
"error": "You only can use this command in MP mode." "error": "You only can use this command in MP mode.",
"description": "Teleports all players in your world to your position"
}, },
"teleport": { "teleport": {
"usage_server": "Usage: /tp @<player id> <x> <y> <z> [scene id]", "usage_server": "Usage: /tp @<player id> <x> <y> <z> [scene id]",
"usage": "Usage: /tp [@<player id>] <x> <y> <z> [scene id]", "usage": "Usage: /tp [@<player id>] <x> <y> <z> [scene id]",
"specify_player_id": "You must specify a player id.", "specify_player_id": "You must specify a player id.",
"invalid_position": "Invalid position.", "invalid_position": "Invalid position.",
"success": "Teleported %s to %s, %s, %s in scene %s" "success": "Teleported %s to %s, %s, %s in scene %s",
"description": "Change the player's position."
}, },
"tower": { "tower": {
"unlock_done": "Abyss Corridor's Floors are all unlocked now." "unlock_done": "Abyss Corridor's Floors are all unlocked now."
...@@ -286,16 +318,22 @@ ...@@ -286,16 +318,22 @@
"weather": { "weather": {
"usage": "Usage: weather <weatherId> [climateId]", "usage": "Usage: weather <weatherId> [climateId]",
"success": "Changed weather to %s with climate %s", "success": "Changed weather to %s with climate %s",
"invalid_id": "Invalid ID." "invalid_id": "Invalid ID.",
"description": "Changes the weather."
}, },
"drop": { "drop": {
"command_usage": "Usage: drop <itemId|itemName> [amount]", "command_usage": "Usage: drop <itemId|itemName> [amount]",
"success": "Dropped %s of %s." "success": "Dropped %s of %s.",
"description": "Drops an item near you"
}, },
"help": { "help": {
"usage": "Usage: ", "usage": "Usage: ",
"aliases": "Aliases: ", "aliases": "Aliases: ",
"available_commands": "Available commands: " "available_commands": "Available commands: ",
"description": "Sends the help message or shows information about a specified command"
},
"restart": {
"description": "Restarts the current session"
} }
} }
} }
\ No newline at end of file
{
"messages": {
"game": {
"port_bind": "Serwer gry uruchomiony na porcie: %s",
"connect": "Klient połączył się z %s",
"disconnect": "Klient rozłączył się z %s",
"game_update_error": "Wystąpił błąd podczas aktualizacji gry.",
"command_error": "Błąd komendy:"
},
"dispatch": {
"port_bind": "[Dispatch] Serwer dispatch wystartował na porcie %s",
"request": "[Dispatch] Klient %s %s zapytanie: %s",
"keystore": {
"general_error": "[Dispatch] Błąd łądowania keystore!",
"password_error": "[Dispatch] Nie można załadować keystore. Próba z domyślnym hasłem keystore...",
"no_keystore_error": "[Dispatch] Brak certyfikatu SSL! Przejście na serwer HTTP.",
"default_password": "[Dispatch] Domyślne hasło keystore zadziałało. Rozważ ustawienie go na 123456 w pliku config.json."
},
"no_commands_error": "Komendy nie są wspierane w trybie DISPATCH_ONLY.",
"unhandled_request_error": "[Dispatch] Potencjalnie niepodtrzymane %s zapytanie: %s",
"account": {
"login_attempt": "[Dispatch] Klient %s próbuje się zalogować",
"login_success": "[Dispatch] Klient %s zalogował się jako %s",
"login_token_attempt": "[Dispatch] Klient %s próbuje się zalogować poprzez token",
"login_token_error": "[Dispatch] Klient %s nie mógł się zalogować poprzez token",
"login_token_success": "[Dispatch] Klient %s zalogował się poprzez token jako %s",
"combo_token_success": "[Dispatch] Klient %s pomyślnie wymienił combo token",
"combo_token_error": "[Dispatch] Klient %s nie wymienił combo token'u",
"account_login_create_success": "[Dispatch] Klient %s nie mógł się zalogować: Konto %s stworzone",
"account_login_create_error": "[Dispatch] Klient %s nie mógł się zalogować: Tworzenie konta nie powiodło się",
"account_login_exist_error": "[Dispatch] Klient %s nie mógł się zalogować: Nie znaleziono konta",
"account_cache_error": "Błąd pamięci cache konta gry",
"session_key_error": "Błędny klucz sesji.",
"username_error": "Nazwa użytkownika nie znaleziona.",
"username_create_error": "Nazwa użytkownika nie znaleziona, tworzenie nie powiodło się."
}
},
"status": {
"free_software": "Grasscutter to DARMOWE oprogramowanie. Jeżeli ktoś Ci je sprzedał, to zostałeś oscamowany. Strona domowa: https://github.com/Grasscutters/Grasscutter",
"starting": "Uruchamianie Grasscutter...",
"shutdown": "Wyłączanie...",
"done": "Gotowe! Wpisz \"help\" aby uzyskać pomoc",
"error": "Wystąpił błąd.",
"welcome": "Witamy w Grasscutter",
"run_mode_error": "Błędny tryb pracy serwera: %s.",
"run_mode_help": "Tryb pracy serwera musi być ustawiony na 'HYBRID', 'DISPATCH_ONLY', lub 'GAME_ONLY'. Nie można wystartować Grasscutter...",
"create_resources": "Tworzenie folderu resources...",
"resources_error": "Umieść kopię 'BinOutput' i 'ExcelBinOutput' w folderze resources."
}
},
"commands": {
"generic": {
"not_specified": "Nie podano komendy.",
"unknown_command": "Nieznana komenda: %s",
"permission_error": "Nie masz uprawnień do tej komendy.",
"console_execute_error": "Tą komende można wywołać tylko z konsoli.",
"player_execute_error": "Wywołaj tą komendę w grze.",
"command_exist_error": "Nie znaleziono komendy.",
"invalid": {
"amount": "Błędna ilość.",
"artifactId": "Błędne ID artefaktu.",
"avatarId": "Błędne id postaci.",
"avatarLevel": "Błędny poziom postaci.",
"entityId": "Błędne id obiektu.",
"id przedmiotu": "Błędne id przedmiotu.",
"itemLevel": "Błędny poziom przedmiotu.",
"itemRefinement": "Błędne ulepszenie.",
"playerId": "Błędne playerId.",
"uid": "Błędne UID."
}
},
"execution": {
"uid_error": "Błędne UID.",
"player_exist_error": "Gracz nie znaleziony.",
"player_offline_error": "Gracz nie jest online.",
"item_id_error": "Błędne ID przedmiotu.",
"item_player_exist_error": "Błędny przedmiot lub UID.",
"entity_id_error": "Błędne ID obiektu.",
"player_exist_offline_error": "Gracz nie znaleziony lub jest offline.",
"argument_error": "Błędne argumenty.",
"clear_target": "Cel wyczyszczony.",
"set_target": "Następne komendy będą celować w @%s.",
"need_target": "Ta komenda wymaga docelowego UID. Dodaj argument <@UID> lub ustaw stały cel poleceniem /target @UID."
},
"status": {
"enabled": "Włączone",
"disabled": "Wyłączone",
"help": "Pomoc",
"success": "Sukces"
},
"account": {
"modify": "Modyfikuj konta użytkowników",
"invalid": "Błędne UID.",
"exists": "Konto już istnieje.",
"create": "Stworzono konto z UID %s.",
"delete": "Konto usunięte.",
"no_account": "Nie znaleziono konta.",
"command_usage": "Użycie: account <create|delete> <nazwa> [uid]"
},
"broadcast": {
"command_usage": "Użycie: broadcast <wiadomość>",
"message_sent": "Wiadomość wysłana."
},
"changescene": {
"usage": "Użycie: changescene <id sceny>",
"already_in_scene": "Już jesteś na tej scenie.",
"success": "Zmieniono scene na %s.",
"exists_error": "Ta scena nie istenieje."
},
"clear": {
"command_usage": "Użycie: clear <all|wp|art|mat>",
"weapons": "Wyczyszczono bronie dla %s.",
"artifacts": "Wyczyszczono artefakty dla %s.",
"materials": "Wyczyszczono materiały dla %s.",
"furniture": "Wyczyszczono meble dla %s.",
"displays": "Wyczyszczono displays dla %s.",
"virtuals": "Wyczyszczono virtuals dla %s.",
"everything": "Wyczyszczono wszystko dla %s."
},
"coop": {
"usage": "Użycie: coop <id gracza> <id gracza docelowego>",
"success": "Przyzwano %s do świata %s."
},
"enter_dungeon": {
"usage": "Użycie: enterdungeon <ID lochu>",
"changed": "Zmieniono loch na %s",
"not_found_error": "Ten loch nie istnieje",
"in_dungeon_error": "Już jesteś w tym lochu"
},
"giveAll": {
"usage": "Użycie: giveall [gracz] [ilość]",
"started": "Dodawanie wszystkich przedmiotów...",
"success": "Pomyślnie dodano wszystkie przedmioty dla %s.",
"invalid_amount_or_playerId": "Błędna ilość lub ID gracza."
},
"giveArtifact": {
"usage": "Użycie: giveart|gart [gracz] <id artefaktu> <mainPropId> [<appendPropId>[,<razy>]]... [poziom]",
"id_error": "Błędne ID artefaktu.",
"success": "Dano %s dla %s."
},
"giveChar": {
"usage": "Użycie: givechar <gracz> <id przedmiotu | nazwa przedmiotu> [ilość]",
"given": "Dano %s z poziomem %s dla %s.",
"invalid_avatar_id": "Błędne ID postaci.",
"invalid_avatar_level": "Błędny poziom postaci.",
"invalid_avatar_or_player_id": "Błędne ID postaci lub gracza."
},
"give": {
"usage": "Użycie: give <gracz> <id przedmiotu | nazwa przedmiotu> [ilość] [poziom]",
"refinement_only_applicable_weapons": "Ulepszenie można zastosować tylko dla broni.",
"refinement_must_between_1_and_5": "Ulepszenie musi być pomiędzy 1, a 5.",
"given": "Dano %s %s dla %s.",
"given_with_level_and_refinement": "Dano %s z poziomem %s, ulepszeniem %s %s razy dla %s",
"given_level": "Dano %s z poziomem %s %s razy dla %s"
},
"godmode": {
"success": "Godmode jest teraz %s dla %s."
},
"heal": {
"success": "Wszystkie postacie zostały wyleczone."
},
"kick": {
"player_kick_player": "Gracz [%s:%s] wyrzucił gracza [%s:%s]",
"server_kick_player": "Wyrzucono gracza [%s:%s]"
},
"kill": {
"usage": "Użycie: killall [UID gracza] [ID sceny]",
"scene_not_found_in_player_world": "Scena nie znaleziona w świecie gracza",
"kill_monsters_in_scene": "Zabito %s potworów w scenie %s"
},
"killCharacter": {
"usage": "Użycie: /killcharacter [ID gracza]",
"success": "Zabito aktualną postać gracza %s."
},
"list": {
"success": "Teraz jest %s gracz(y) online:"
},
"permission": {
"usage": "Użycie: permission <add|remove> <nazwa gracza> <uprawnienie>",
"add": "Dodano uprawnienie",
"has_error": "To konto już ma to uprawnienie!",
"remove": "Usunięto uprawnienie.",
"not_have_error": "To konto nie ma tych uprawnień!",
"account_error": "Konto nie może zostać znalezione."
},
"position": {
"success": "Koordynaty: %.3f, %.3f, %.3f\nID sceny: %d"
},
"reload": {
"reload_start": "Ponowne ładowanie konfiguracji.",
"reload_done": "Ponowne ładowanie zakończone."
},
"resetConst": {
"reset_all": "Resetuj konstelacje wszystkich postaci.",
"success": "Konstelacje dla %s zostały zresetowane. Proszę zalogować się ponownie aby zobaczyć zmiany."
},
"resetShopLimit": {
"usage": "Użycie: /resetshop <ID gracza>"
},
"sendMail": {
"usage": "Użycie: `/sendmail <ID gracza | all | help> [id szablonu]`",
"user_not_exist": "Gracz o ID '%s' nie istnieje",
"start_composition": "Komponowanie wiadomości.\nProszę użyj `/sendmail <tytuł>` aby kontynuować.\nMożesz użyć `/sendmail stop` w dowolnym momencie",
"templates": "Szablony zostaną zaimplementowane niedługo...",
"invalid_arguments": "Błędne argumenty.\nUżycie `/sendmail <ID gracza | all | help> [id szablonu]`",
"send_cancel": "Anulowano wysyłanie wiadomości",
"send_done": "Wysłano wiadomość do gracza %s!",
"send_all_done": "Wysłano wiadomośc do wszystkich graczy!",
"not_composition_end": "Komponowanie nie jest na ostatnim etapie.\nProszę użyj `/sendmail %s` lub `/sendmail stop` aby anulować",
"please_use": "Proszę użyj `/sendmail %s`",
"set_title": "Tytuł wiadomości to teraz: '%s'.\nUżyj '/sendmail <treść>' aby kontynuować.",
"set_contents": "Treść wiadomości to teraz '%s'.\nUżyj '/sendmail <nadawca>' aby kontynuować.",
"set_message_sender": "Nadawca wiadomości to teraz '%s'.\nUżyj '/sendmail <id przedmiotu | nazwa przedmiotu | zakończ> [ilość] [poziom]' aby kontynuować.",
"send": "Załączono %s %s (poziom %s) do wiadomości.\nDodaj więcej przedmiotów lub użyj `/sendmail finish` aby wysłać wiadomość.",
"invalid_arguments_please_use": "Błędne argumenty \nProszę użyj `/sendmail %s`",
"title": "<tytuł>",
"message": "<wiadomość>",
"sender": "<nadawca>",
"arguments": "<id przedmiotu | nazwa przedmiotu | zakończ> [ilość] [poziom]",
"error": "BŁĄD: niepoprawny etap konstrukcji: %s. Sprawdź konsolę aby dowiedzieć się więcej."
},
"sendMessage": {
"usage": "Użycie: /sendmessage <player> <message>",
"success": "Wiadomość wysłana."
},
"setFetterLevel": {
"usage": "Użycie: setfetterlevel <poziom>",
"range_error": "Poziom przyjaźni musi być pomiędzy 0,a 10.",
"success": "Poziom przyjaźni ustawiono na: %s",
"level_error": "Błędny poziom przyjaźni."
},
"setStats": {
"usage_console": "Użycie: setstats|stats @<UID> <statystyka> <wartość>",
"usage_ingame": "Użycie: setstats|stats [@UID] <statystyka> <wartość>",
"help_message": "\n\tWartości dla Statystyka: hp | maxhp | def | atk | em | er | crate | cdmg | cdr | heal | heali | shield | defi\n\t(cont.) Bonus DMG żywiołu: epyro | ecryo | ehydro | egeo | edendro | eelectro | ephys\n\t(cont.) RES na żywioł: respyro | rescryo | reshydro | resgeo | resdendro | reselectro | resphys\n",
"value_error": "Błędna wartość statystyki.",
"uid_error": "Błędne UID.",
"player_error": "Gracza nie znaleziono lub jest offline.",
"set_self": "%s ustawiono na %s.",
"set_for_uid": "%s dla %s ustawiono na %s.",
"set_max_hp": "Maksymalne HP ustawione na %s."
},
"setWorldLevel": {
"usage": "Użycie: setworldlevel <poziom>",
"value_error": "Poziom świata musi być pomiędzy 0, a 8",
"success": "Ustawiono poziom świata na: %s.",
"invalid_world_level": "Invalid world level."
},
"spawn": {
"usage": "Użycie: /spawn <id obiektu> [ilość] [poziom(tylko potwory)]",
"success": "Stworzono %s %s."
},
"stop": {
"success": "Serwer wyłącza się..."
},
"talent": {
"usage_1": "Aby ustawić poziom talentu: /talent set <ID talentu> <wartość>",
"usage_2": "Inny sposób na ustawienie poziomu talentu: /talent <n lub e lub q> <wartość>",
"usage_3": "Aby uzyskać ID talentu: /talent getid",
"lower_16": "Błędny poziom talentu. Poziom powinien być mniejszy niż 16",
"set_id": "Ustawiono talent na %s.",
"set_atk": "Ustawiono talent Atak Podstawowy na poziom %s.",
"set_e": "Ustawiono poziom talentu E na %s.",
"set_q": "Ustawiono poziom talentu Q na %s.",
"invalid_skill_id": "Błędne ID umiejętności.",
"set_this": "Ustawiono ten talent na poziom %s.",
"invalid_level": "Błędny poziom talentu.",
"normal_attack_id": "ID podstawowego ataku: %s.",
"e_skill_id": "ID umiejętności E: %s.",
"q_skill_id": "ID umiejętności Q: %s."
},
"teleportAll": {
"success": "Przyzwano wszystkich graczy do Ciebie.",
"error": "Możesz użyć tej komendy wyłącznie w trybie MP."
},
"teleport": {
"usage_server": "Użycie: /tp @<ID gracza> <x> <y> <z> [ID sceny]",
"usage": "Użycie: /tp [@<ID gracza>] <x> <y> <z> [ID sceny]",
"specify_player_id": "Musisz określić ID gracza.",
"invalid_position": "Błędna pozycja.",
"success": "Przeteleportowano %s do %s, %s, %s w scenie %s"
},
"weather": {
"usage": "Użycie: weather <ID pogody> [ID klimatu]",
"success": "Zmieniono pogodę na %s z klimatem %s",
"invalid_id": "Błędne ID."
},
"drop": {
"command_usage": "Użycie: drop <ID przedmiotu|nazwa przedmiotu> [ilość]",
"success": "Wyrzucono %s of %s."
},
"help": {
"usage": "Użycie: ",
"aliases": "Aliasy: ",
"available_commands": "Dostępne komendy: "
}
}
}
\ No newline at end of file
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