Commit d468edcf authored by Akka's avatar Akka
Browse files

merge

parents 4b6842f0 3a5503de
......@@ -10,9 +10,9 @@ import java.util.List;
import static emu.grasscutter.utils.Language.translate;
@Command(label = "tpall", usage = "tpall",
description = "Teleports all players in your world to your position", permission = "player.tpall")
@Command(label = "tpall", usage = "tpall", permission = "player.tpall", description = "commands.teleportAll.description")
public final class TeleportAllCommand implements CommandHandler {
@Override
public void execute(Player sender, Player targetPlayer, List<String> args) {
if (targetPlayer == null) {
......
......@@ -10,8 +10,7 @@ import java.util.List;
import static emu.grasscutter.utils.Language.translate;
@Command(label = "teleport", usage = "teleport <x> <y> <z> [scene id]", aliases = {"tp"},
description = "Change the player's position.", permission = "player.teleport")
@Command(label = "teleport", usage = "teleport <x> <y> <z> [scene id]", aliases = {"tp"}, permission = "player.teleport", description = "commands.teleport.description")
public final class TeleportCommand implements CommandHandler {
private float parseRelative(String input, Float current) { // TODO: Maybe this will be useful elsewhere later
......
......@@ -11,8 +11,7 @@ import java.util.List;
import static emu.grasscutter.utils.Language.translate;
@Command(label = "weather", usage = "weather <weatherId> [climateId]",
description = "Changes the weather.", aliases = {"w"}, permission = "player.weather")
@Command(label = "weather", usage = "weather <weatherId> [climateId]", aliases = {"w"}, permission = "player.weather", description = "commands.weather.description")
public final class WeatherCommand implements CommandHandler {
@Override
......
......@@ -96,7 +96,7 @@ public class GachaBanner {
return toProto("");
}
public GachaInfo toProto(String sessionKey) {
String record = "https://"
String record = "http" + (Grasscutter.getConfig().getDispatchOptions().FrontHTTPS ? "s" : "") + "://"
+ (Grasscutter.getConfig().getDispatchOptions().PublicIp.isEmpty() ?
Grasscutter.getConfig().getDispatchOptions().Ip :
Grasscutter.getConfig().getDispatchOptions().PublicIp)
......
package emu.grasscutter.game.managers.SotSManager;
package emu.grasscutter.game.managers;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.game.avatar.Avatar;
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.props.FightProperty;
import emu.grasscutter.game.props.PlayerProperty;
import emu.grasscutter.game.world.World;
import emu.grasscutter.net.proto.ChangeHpReasonOuterClass;
import emu.grasscutter.net.proto.PropChangeReasonOuterClass;
import emu.grasscutter.server.game.GameSession;
......@@ -29,6 +26,8 @@ public class SotSManager {
private final Player player;
private Timer autoRecoverTimer;
public final static int GlobalMaximumSpringVolume = 8500000;
public SotSManager(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.game.entity.EntityAvatar;
......@@ -7,81 +7,43 @@ import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.props.FightProperty;
import emu.grasscutter.game.props.LifeState;
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.MotionStateOuterClass.MotionState;
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.packet.send.*;
import emu.grasscutter.utils.Position;
import org.jetbrains.annotations.NotNull;
import java.lang.Math;
import java.util.*;
public class MovementManager {
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);
public class StaminaManager {
private final Player player;
private HashMap<String, HashSet<MotionState>> MotionStatesCategorized = new HashMap<>();
private float landSpeed = 0;
private long landTimeMillisecond = 0;
private Timer movementManagerTickTimer;
public final static int GlobalMaximumStamina = 24000;
private Position currentCoordinates = new Position(0, 0, 0);
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 GameEntity cachedEntity = null;
private int staminaRecoverDelay = 0;
private int skillCaster = 0;
private int skillCasting = 0;
private boolean isInSkillMove = false;
public boolean getIsInSkillMove() {
return isInSkillMove;
}
public void setIsInSkillMove(boolean b) {
isInSkillMove = b;
}
public MovementManager(Player player) {
previousCoordinates.add(new Position(0,0,0));
public StaminaManager(Player player) {
this.player = player;
MotionStatesCategorized.put("SWIM", new HashSet<>(Arrays.asList(
......@@ -129,252 +91,227 @@ public class MovementManager {
)));
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() {
float diffX = currentCoordinates.getX() - previousCoordinates.getX();
float diffY = currentCoordinates.getY() - previousCoordinates.getY();
float diffZ = currentCoordinates.getZ() - previousCoordinates.getZ();
// Grasscutter.getLogger().debug("isPlayerMoving: " + previousCoordinates + ", " + currentCoordinates + ", " + diffX + ", " + diffY + ", " + diffZ);
return Math.abs(diffX) > 0.2 || Math.abs(diffY) > 0.1 || Math.abs(diffZ) > 0.2;
}
private int getCurrentStamina() {
return player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA);
}
private int getMaximumStamina() {
return player.getProperty(PlayerProperty.PROP_MAX_STAMINA);
Grasscutter.getLogger().debug("isPlayerMoving: " + previousCoordinates + ", " + currentCoordinates +
", " + diffX + ", " + diffY + ", " + diffZ);
return Math.abs(diffX) > 0.3 || Math.abs(diffY) > 0.2 || Math.abs(diffZ) > 0.3;
}
// Returns new stamina
public int updateStamina(GameSession session, int amount) {
int currentStamina = session.getPlayer().getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA);
if (amount == 0) {
// Returns new stamina and sends PlayerPropNotify
public int updateStamina(GameSession session, Consumption consumption) {
int currentStamina = player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA);
if (consumption.amount == 0) {
return currentStamina;
}
int playerMaxStamina = session.getPlayer().getProperty(PlayerProperty.PROP_MAX_STAMINA);
int newStamina = currentStamina + amount;
int playerMaxStamina = player.getProperty(PlayerProperty.PROP_MAX_STAMINA);
Grasscutter.getLogger().debug(currentStamina + "/" + playerMaxStamina + "\t" + currentState + "\t" +
(isPlayerMoving() ? "moving" : " ") + "\t(" + consumption.consumptionType + "," +
consumption.amount + ")");
int newStamina = currentStamina + consumption.amount;
if (newStamina < 0) {
newStamina = 0;
}
if (newStamina > playerMaxStamina) {
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));
return newStamina;
}
private void handleFallOnGround(@NotNull MotionInfo motionInfo) {
MotionState state = motionInfo.getState();
// land speed and fall on ground event arrive in different packets
// cache land speed
if (state == MotionState.MOTION_LAND_SPEED) {
landSpeed = motionInfo.getSpeed().getY();
landTimeMillisecond = System.currentTimeMillis();
// Kills avatar, removes entity and sends notification.
// TODO: Probably move this to Avatar class? since other components may also need to kill avatar.
public void killAvatar(GameSession session, GameEntity entity, PlayerDieType dieType) {
session.send(new PacketAvatarLifeStateChangeNotify(player.getTeamManager().getCurrentAvatarEntity().getAvatar(),
LifeState.LIFE_DEAD, dieType));
session.send(new PacketLifeStateChangeNotify(entity, LifeState.LIFE_DEAD, dieType));
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.
int maxDelay = 200;
if ((System.currentTimeMillis() - landTimeMillisecond) > maxDelay) {
Grasscutter.getLogger().debug("MOTION_FALL_ON_GROUND received after " + maxDelay + "ms, discard.");
return;
}
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;
}
public void stopSustainedStaminaHandler() {
if (timerRunning) {
timerRunning = false;
sustainedStaminaHandlerTimer.cancel();
// Grasscutter.getLogger().debug("[MovementManager] SustainedStaminaHandlerTimer stopped");
}
}
private void handleDrowning() {
int stamina = getCurrentStamina();
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);
// Handlers
// External trigger handler
public void handleEvtDoSkillSuccNotify(GameSession session, EvtDoSkillSuccNotify notify) {
handleImmediateStamina(session, notify);
}
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) {
cachedSession.send(new PacketAvatarLifeStateChangeNotify(
cachedSession.getPlayer().getTeamManager().getCurrentAvatarEntity().getAvatar(),
LifeState.LIFE_DEAD,
dieType
));
cachedSession.send(new PacketLifeStateChangeNotify(
cachedEntity,
LifeState.LIFE_DEAD,
dieType
));
cachedEntity.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, 0);
cachedEntity.getWorld().broadcastPacket(new PacketEntityFightPropUpdateNotify(cachedEntity, FightProperty.FIGHT_PROP_CUR_HP));
entity.getWorld().broadcastPacket(new PacketLifeStateChangeNotify(0, entity, LifeState.LIFE_DEAD));
session.getPlayer().getScene().removeEntity(entity);
((EntityAvatar)entity).onDeath(dieType, 0);
// Internal handler
private void handleImmediateStamina(GameSession session, MotionInfo motionInfo, MotionState motionState,
GameEntity entity) {
switch (motionState) {
case MOTION_DASH_BEFORE_SHAKE:
if (previousState != MotionState.MOTION_DASH_BEFORE_SHAKE) {
updateStamina(session, new Consumption(ConsumptionType.SPRINT));
}
break;
case MOTION_CLIMB_JUMP:
if (previousState != MotionState.MOTION_CLIMB_JUMP) {
updateStamina(session, new Consumption(ConsumptionType.CLIMB_JUMP));
}
break;
case MOTION_SWIM_DASH:
if (previousState != MotionState.MOTION_SWIM_DASH) {
updateStamina(session, new Consumption(ConsumptionType.SWIM_DASH_START));
}
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() {
if (Grasscutter.getConfig().OpenStamina) {
boolean moving = isPlayerMoving();
if (moving || (getCurrentStamina() < getMaximumStamina())) {
// Grasscutter.getLogger().debug("Player moving: " + moving + ", stamina full: " + (getCurrentStamina() >= getMaximumStamina()) + ", recalculate stamina");
int currentStamina = player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA);
int maxStamina = player.getProperty(PlayerProperty.PROP_MAX_STAMINA);
if (moving || (currentStamina < maxStamina)) {
Grasscutter.getLogger().debug("Player moving: " + moving + ", stamina full: " +
(currentStamina >= maxStamina) + ", recalculate stamina");
Consumption consumption = new Consumption(ConsumptionType.None);
// TODO: refactor these conditions.
if (MotionStatesCategorized.get("CLIMB").contains(currentState)) {
consumption = getClimbConsumption();
} else if (MotionStatesCategorized.get("SWIM").contains((currentState))) {
consumption = getSwimConsumptions();
} else if (MotionStatesCategorized.get("RUN").contains(currentState)) {
consumption = getRunWalkDashConsumption();
} else if (MotionStatesCategorized.get("FLY").contains(currentState)) {
consumption = getFlyConsumption();
} else if (MotionStatesCategorized.get("STANDBY").contains(currentState)) {
consumption = getStandConsumption();
} else if (MotionStatesCategorized.get("FIGHT").contains(currentState)) {
consumption = getFightConsumption();
if (!isInSkillMove) {
if (MotionStatesCategorized.get("CLIMB").contains(currentState)) {
consumption = getClimbSustainedConsumption();
} else if (MotionStatesCategorized.get("SWIM").contains((currentState))) {
consumption = getSwimSustainedConsumptions();
} else if (MotionStatesCategorized.get("RUN").contains(currentState)) {
consumption = getRunWalkDashSustainedConsumption();
} else if (MotionStatesCategorized.get("FLY").contains(currentState)) {
consumption = getFlySustainedConsumption();
} else if (MotionStatesCategorized.get("STANDBY").contains(currentState)) {
consumption = getStandSustainedConsumption();
}
}
// delay 2 seconds before start recovering - as official server does.
if (cachedSession != null) {
if (consumption.amount < 0) {
staminaRecoverDelay = 0;
}
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) {
// For others recover after 2 seconds (10 ticks) - as official server does.
staminaRecoverDelay++;
consumption = new Consumption(ConsumptionType.None);
}
}
// Grasscutter.getLogger().debug(getCurrentStamina() + "/" + getMaximumStamina() + "\t" + currentState + "\t" + "isMoving: " + isPlayerMoving() + "\t(" + consumption.consumptionType + "," + consumption.amount + ")");
updateStamina(cachedSession, consumption.amount);
updateStamina(cachedSession, consumption);
}
// tick triggered
handleDrowning();
}
}
previousState = currentState;
previousCoordinates = new Position(currentCoordinates.getX(),
currentCoordinates.getY(), currentCoordinates.getZ());;
previousCoordinates = new Position(
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);
if (currentState == MotionState.MOTION_CLIMB) {
if (currentState == MotionState.MOTION_CLIMB && isPlayerMoving()) {
consumption = new Consumption(ConsumptionType.CLIMBING);
if (previousState != MotionState.MOTION_CLIMB && previousState != MotionState.MOTION_CLIMB_JUMP) {
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;
}
private Consumption getSwimConsumptions() {
private Consumption getSwimSustainedConsumptions() {
Consumption consumption = new Consumption(ConsumptionType.None);
if (currentState == MotionState.MOTION_SWIM_MOVE) {
consumption = new Consumption(ConsumptionType.SWIMMING);
}
if (currentState == MotionState.MOTION_SWIM_DASH) {
consumption = new Consumption(ConsumptionType.SWIM_DASH_START);
if (previousState == MotionState.MOTION_SWIM_DASH) {
consumption = new Consumption(ConsumptionType.SWIM_DASH);
}
consumption = new Consumption(ConsumptionType.SWIM_DASH);
}
return consumption;
}
private Consumption getRunWalkDashConsumption() {
private Consumption getRunWalkDashSustainedConsumption() {
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) {
consumption = new Consumption(ConsumptionType.SPRINT);
consumption = new Consumption(ConsumptionType.DASH);
}
if (currentState == MotionState.MOTION_RUN) {
consumption = new Consumption(ConsumptionType.RUN);
......@@ -385,22 +322,21 @@ public class MovementManager {
return consumption;
}
private Consumption getFlyConsumption() {
private Consumption getFlySustainedConsumption() {
Consumption consumption = new Consumption(ConsumptionType.FLY);
HashMap<Integer, Float> glidingCostReduction = new HashMap<>() {{
put(212301, 0.8f); // Amber
put(222301, 0.8f); // Venti
}};
float reduction = 1;
for (EntityAvatar entity: cachedSession.getPlayer().getTeamManager().getActiveTeam()) {
for (int skillId: entity.getAvatar().getProudSkillList()) {
for (EntityAvatar entity : cachedSession.getPlayer().getTeamManager().getActiveTeam()) {
for (int skillId : entity.getAvatar().getProudSkillList()) {
if (glidingCostReduction.containsKey(skillId)) {
reduction = glidingCostReduction.get(skillId);
}
}
}
consumption.amount *= reduction;
// POWERED_FLY, e.g. wind tunnel
if (currentState == MotionState.MOTION_POWERED_FLY) {
consumption = new Consumption(ConsumptionType.POWERED_FLY);
......@@ -408,7 +344,7 @@ public class MovementManager {
return consumption;
}
private Consumption getStandConsumption() {
private Consumption getStandSustainedConsumption() {
Consumption consumption = new Consumption(ConsumptionType.None);
if (currentState == MotionState.MOTION_STANDBY) {
consumption = new Consumption(ConsumptionType.STANDBY);
......@@ -418,25 +354,4 @@ public class MovementManager {
}
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;
import emu.grasscutter.game.inventory.Inventory;
import emu.grasscutter.game.mail.Mail;
import emu.grasscutter.game.mail.MailHandler;
import emu.grasscutter.game.managers.MovementManager.MovementManager;
import emu.grasscutter.game.managers.SotSManager.SotSManager;
import emu.grasscutter.game.managers.StaminaManager.StaminaManager;
import emu.grasscutter.game.managers.SotSManager;
import emu.grasscutter.game.props.ActionReason;
import emu.grasscutter.game.props.EntityType;
import emu.grasscutter.game.props.PlayerProperty;
......@@ -62,9 +62,6 @@ import java.util.concurrent.LinkedBlockingQueue;
@Entity(value = "players", useDiscriminator = false)
public class Player {
@Transient private static int GlobalMaximumSpringVolume = 8500000;
@Transient private static int GlobalMaximumStamina = 24000;
@Id private int id;
@Indexed(options = @IndexOptions(unique = true)) private String accountId;
......@@ -132,7 +129,7 @@ public class Player {
@Transient private final InvokeHandler<AbilityInvokeEntry> clientAbilityInitFinishHandler;
private MapMarksManager mapMarksManager;
@Transient private MovementManager movementManager;
@Transient private StaminaManager staminaManager;
private long springLastUsed;
......@@ -178,7 +175,7 @@ public class Player {
this.expeditionInfo = new HashMap<>();
this.messageHandler = null;
this.mapMarksManager = new MapMarksManager();
this.movementManager = new MovementManager(this);
this.staminaManager = new StaminaManager(this);
this.sotsManager = new SotSManager(this);
}
......@@ -206,7 +203,7 @@ public class Player {
this.getRotation().set(0, 307, 0);
this.messageHandler = null;
this.mapMarksManager = new MapMarksManager();
this.movementManager = new MovementManager(this);
this.staminaManager = new StaminaManager(this);
this.sotsManager = new SotSManager(this);
}
......@@ -875,11 +872,11 @@ public class Player {
}
public void onPause() {
getStaminaManager().stopSustainedStaminaHandler();
}
public void onUnpause() {
getStaminaManager().startSustainedStaminaHandler();
}
public void sendPacket(BasePacket packet) {
......@@ -1024,7 +1021,7 @@ public class Player {
return mapMarksManager;
}
public MovementManager getMovementManager() { return movementManager; }
public StaminaManager getStaminaManager() { return staminaManager; }
public SotSManager getSotSManager() { return sotsManager; }
......@@ -1152,7 +1149,7 @@ public class Player {
public void onLogout() {
// stop stamina calculation
getMovementManager().resetTimer();
getStaminaManager().stopSustainedStaminaHandler();
// force to leave the dungeon
if (getScene().getSceneType() == SceneType.SCENE_DUNGEON) {
......@@ -1214,7 +1211,7 @@ public class Player {
} else if (prop == PlayerProperty.PROP_LAST_CHANGE_AVATAR_TIME) { // 10001
// TODO: implement sanity check
} 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
int playerMaximumSpringVolume = getProperty(PlayerProperty.PROP_MAX_SPRING_VOLUME);
if (!(value >= 0 && value <= playerMaximumSpringVolume)) { return false; }
......@@ -1231,7 +1228,7 @@ public class Player {
} else if (prop == PlayerProperty.PROP_IS_TRANSFERABLE) { // 10009
if (!(0 <= value && value <= 1)) { return false; }
} 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
int playerMaximumStamina = getProperty(PlayerProperty.PROP_MAX_STAMINA);
if (!(value >= 0 && value <= playerMaximumStamina)) { return false; }
......@@ -1242,7 +1239,7 @@ public class Player {
} else if (prop == PlayerProperty.PROP_PLAYER_EXP) { // 10014
if (!(0 <= value)) { return false; }
} else if (prop == PlayerProperty.PROP_PLAYER_HCOIN) { // 10015
// see 10015
// see PlayerProperty.PROP_PLAYER_HCOIN comments
} else if (prop == PlayerProperty.PROP_PLAYER_SCOIN) { // 10016
// See 10015
} else if (prop == PlayerProperty.PROP_PLAYER_MP_SETTING_TYPE) { // 10017
......
......@@ -557,7 +557,7 @@ public class TeamManager {
// return;
// }
// }
player.getMovementManager().resetTimer(); // prevent drowning immediately after respawn
player.getStaminaManager().stopSustainedStaminaHandler(); // prevent drowning immediately after respawn
// Revive all team members
for (EntityAvatar entity : getActiveTeam()) {
......
......@@ -4,12 +4,12 @@ import emu.grasscutter.Grasscutter;
import emu.grasscutter.server.event.Event;
import emu.grasscutter.server.event.EventHandler;
import emu.grasscutter.server.event.HandlerPriority;
import emu.grasscutter.utils.EventConsumer;
import emu.grasscutter.utils.Utils;
import java.io.File;
import java.io.InputStreamReader;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.*;
......@@ -47,12 +47,23 @@ public final class PluginManager {
List<File> plugins = Arrays.stream(files)
.filter(file -> file.getName().endsWith(".jar"))
.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 -> {
try {
URL url = plugin.toURI().toURL();
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());
PluginConfig pluginConfig = Grasscutter.getGsonFactory().fromJson(fileReader, PluginConfig.class);
......@@ -68,10 +79,10 @@ public final class PluginManager {
JarEntry entry = entries.nextElement();
if(entry.isDirectory() || !entry.getName().endsWith(".class") || entry.getName().contains("module-info")) continue;
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();
this.loadPlugin(pluginInstance, PluginIdentifier.fromPluginConfig(pluginConfig), loader);
......@@ -156,6 +167,10 @@ public final class PluginManager {
.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.
* @param event The event passed through to the handler.
......@@ -167,4 +182,4 @@ public final class PluginManager {
(event.isCanceled() && handler.ignoresCanceled())
) handler.getCallback().consume((T) event);
}
}
\ No newline at end of file
}
package emu.grasscutter.server.packet.recv;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.game.entity.GameEntity;
import emu.grasscutter.game.props.FightProperty;
import emu.grasscutter.net.packet.Opcodes;
import emu.grasscutter.net.packet.PacketOpcodes;
import emu.grasscutter.net.proto.CombatInvocationsNotifyOuterClass.CombatInvocationsNotify;
......@@ -8,11 +10,19 @@ import emu.grasscutter.net.proto.CombatInvokeEntryOuterClass.CombatInvokeEntry;
import emu.grasscutter.net.proto.EntityMoveInfoOuterClass.EntityMoveInfo;
import emu.grasscutter.net.proto.EvtBeingHitInfoOuterClass.EvtBeingHitInfo;
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.packet.send.PacketEntityFightPropUpdateNotify;
@Opcodes(PacketOpcodes.CombatInvocationsNotify)
public class HandlerCombatInvocationsNotify extends PacketHandler {
private float cachedLandingSpeed = 0;
private long cachedLandingTimeMillisecond = 0;
private boolean monitorLandingEvent = false;
@Override
public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
CombatInvocationsNotify notif = CombatInvocationsNotify.parseFrom(payload);
......@@ -28,7 +38,33 @@ public class HandlerCombatInvocationsNotify extends PacketHandler {
EntityMoveInfo moveInfo = EntityMoveInfo.parseFrom(entry.getCombatData());
GameEntity entity = session.getPlayer().getScene().getEntityById(moveInfo.getEntityId());
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;
default:
......@@ -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;
import emu.grasscutter.game.managers.SotSManager.SotSManager;
import emu.grasscutter.game.managers.SotSManager;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.props.FightProperty;
import emu.grasscutter.net.packet.Opcodes;
import emu.grasscutter.net.packet.PacketHandler;
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.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)
public class HandlerEnterTransPointRegionNotify extends PacketHandler {
......
package emu.grasscutter.server.packet.recv;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.net.packet.Opcodes;
import emu.grasscutter.net.packet.PacketHandler;
import emu.grasscutter.net.packet.PacketOpcodes;
......@@ -15,10 +14,7 @@ public class HandlerEvtDoSkillSuccNotify extends PacketHandler {
EvtDoSkillSuccNotify notify = EvtDoSkillSuccNotify.parseFrom(payload);
// TODO: Will be used for deducting stamina for charged skills.
int caster = notify.getCasterId();
int skillId = notify.getSkillId();
session.getPlayer().getMovementManager().notifySkill(caster, skillId);
session.getPlayer().getStaminaManager().handleEvtDoSkillSuccNotify(session, notify);
}
}
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.net.packet.Opcodes;
import emu.grasscutter.net.packet.PacketHandler;
......
......@@ -13,16 +13,16 @@ import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.stream.Collectors;
import com.google.gson.reflect.TypeToken;
import emu.grasscutter.GameConstants;
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.ResourceLoader;
import emu.grasscutter.data.def.AvatarData;
......@@ -31,6 +31,8 @@ import emu.grasscutter.data.def.MonsterData;
import emu.grasscutter.data.def.SceneData;
import emu.grasscutter.utils.Utils;
import static emu.grasscutter.utils.Language.translate;
public final class Tools {
public static void createGmHandbook() throws Exception {
ToolsWithLanguageOption.createGmHandbook(getLanguageOption());
......@@ -111,7 +113,20 @@ final class ToolsWithLanguageOption {
writer.println("// Grasscutter " + GameConstants.VERSION + " GM Handbook");
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());
Collections.sort(list);
......
......@@ -19,7 +19,7 @@ public final class Language {
* @return A language instance.
*/
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 {
*/
public static String translate(String key, Object... args) {
String translated = Grasscutter.getLanguage().get(key);
try {
return translated.formatted(args);
} catch (Exception exception) {
......@@ -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.
* @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;
try {
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) {
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;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.time.*;
......@@ -17,6 +18,8 @@ import io.netty.buffer.Unpooled;
import org.slf4j.Logger;
import javax.annotation.Nullable;
import static emu.grasscutter.utils.Language.translate;
@SuppressWarnings({"UnusedReturnValue", "BooleanMethodIsAlwaysInverted"})
......@@ -252,14 +255,18 @@ public final class Utils {
* @param stream The input stream.
* @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();
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) {
stringBuilder.append(line);
} stream.close();
} catch (IOException e) {
Grasscutter.getLogger().warn("Failed to read from input stream.");
} catch (NullPointerException ignored) {
return "empty";
} return stringBuilder.toString();
}
......
......@@ -56,6 +56,7 @@
"console_execute_error": "This command can only be run from the console.",
"player_execute_error": "Run this command in-game.",
"command_exist_error": "No command found.",
"no_description_specified": "No description specified",
"invalid": {
"amount": "Invalid amount.",
"artifactId": "Invalid artifactId.",
......@@ -95,17 +96,20 @@
"create": "Account created with UID %s.",
"delete": "Account deleted.",
"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": {
"command_usage": "Usage: broadcast <message>",
"message_sent": "Message sent."
"message_sent": "Message sent.",
"description": "Sends a message to all the players"
},
"changescene": {
"usage": "Usage: changescene <sceneId>",
"already_in_scene": "You are already in that scene.",
"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": {
"command_usage": "Usage: clear <all|wp|art|mat>",
......@@ -115,35 +119,41 @@
"furniture": "Cleared furniture for %s.",
"displays": "Cleared displays 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": {
"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": {
"usage": "Usage: enterdungeon <dungeon id>",
"changed": "Changed to dungeon %s",
"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": {
"usage": "Usage: giveall [player] [amount]",
"started": "Receiving all items...",
"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": {
"usage": "Usage: giveart|gart [player] <artifactId> <mainPropId> [<appendPropId>[,<times>]]... [level]",
"id_error": "Invalid artifact ID.",
"success": "Given %s to %s."
"success": "Given %s to %s.",
"description": "Gives the player a specified artifact"
},
"giveChar": {
"usage": "Usage: givechar <player> <itemId|itemName> [amount]",
"given": "Given %s with level %s to %s.",
"invalid_avatar_id": "Invalid avatar id.",
"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": {
"usage": "Usage: give <player> <itemId|itemName> [amount] [level]",
......@@ -151,29 +161,36 @@
"refinement_must_between_1_and_5": "Refinement must be between 1 and 5.",
"given": "Given %s of %s 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": {
"success": "Godmode is now %s for %s."
"success": "Godmode is now %s for %s.",
"description": "Prevents you from taking damage. Defaults to toggle."
},
"heal": {
"success": "All characters have been healed."
"success": "All characters have been healed.",
"description": "Heal all characters in your current team."
},
"kick": {
"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": {
"usage": "Usage: killall [playerUid] [sceneId]",
"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": {
"usage": "Usage: /killcharacter [playerId]",
"success": "Killed %s's current character."
"success": "Killed %s's current character.",
"description": "Kills the players current character"
},
"list": {
"success": "There are %s player(s) online:"
"success": "There are %s player(s) online:",
"description": "List online players"
},
"permission": {
"usage": "Usage: permission <add|remove> <username> <permission>",
......@@ -181,21 +198,26 @@
"has_error": "They already have this permission!",
"remove": "Permission removed.",
"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": {
"success": "Coordinates: %.3f, %.3f, %.3f\nScene id: %d"
"success": "Coordinates: %s, %s, %s\nScene id: %s",
"description": "Get coordinates."
},
"reload": {
"reload_start": "Reloading config.",
"reload_done": "Reload complete."
"reload_done": "Reload complete.",
"description": "Reload server config"
},
"resetConst": {
"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": {
"usage": "Usage: /resetshop <player id>"
"usage": "Usage: /resetshop <player id>",
"description": "Reset target player's shop refresh time."
},
"sendMail": {
"usage": "Usage: give [player] <itemId|itemName> [amount]",
......@@ -217,17 +239,20 @@
"message": "<message>",
"sender": "<sender>",
"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": {
"usage": "Usage: sendmessage <player> <message>",
"success": "Message sent."
"success": "Message sent.",
"description": "Sends a message to a player as the server"
},
"setFetterLevel": {
"usage": "Usage: setfetterlevel <level>",
"range_error": "Fetter level must be between 0 and 10.",
"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": {
"usage_console": "Usage: setstats|stats @<UID> <stat> <value>",
......@@ -238,20 +263,24 @@
"player_error": "Player not found or offline.",
"set_self": "%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": {
"usage": "Usage: setworldlevel <level>",
"value_error": "World level must be between 0-8",
"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": {
"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": {
"success": "Server shutting down..."
"success": "Server shutting down...",
"description": "Stops the server"
},
"talent": {
"usage_1": "To set talent level: /talent set <talentID> <value>",
......@@ -267,18 +296,21 @@
"invalid_level": "Invalid talent level.",
"normal_attack_id": "Normal Attack 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": {
"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": {
"usage_server": "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.",
"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": {
"unlock_done": "Abyss Corridor's Floors are all unlocked now."
......@@ -286,16 +318,22 @@
"weather": {
"usage": "Usage: weather <weatherId> [climateId]",
"success": "Changed weather to %s with climate %s",
"invalid_id": "Invalid ID."
"invalid_id": "Invalid ID.",
"description": "Changes the weather."
},
"drop": {
"command_usage": "Usage: drop <itemId|itemName> [amount]",
"success": "Dropped %s of %s."
"success": "Dropped %s of %s.",
"description": "Drops an item near you"
},
"help": {
"usage": "Usage: ",
"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
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