Commit a8293102 authored by Melledy's avatar Melledy Committed by GitHub
Browse files

Merge branch 'development' into stable

parents 304b9cb8 ecf7a81a
# Stamina Manager
---
## UpdateStamina
```java
// will use consumption.consumptionType as reason
public int updateStaminaRelative(GameSession session, Consumption consumption);
```
```java
public int updateStaminaAbsolute(GameSession session, String reason, int newStamina)
```
---
## Pause and Resume
```java
public void startSustainedStaminaHandler()
```
```java
public void stopSustainedStaminaHandler()
```
---
## Stamina change listeners and intercepting
### BeforeUpdateStaminaListener
```java
import emu.grasscutter.game.managers.StaminaManager.BeforeUpdateStaminaListener;
// Listener sample: plugin disable CLIMB_JUMP stamina cost.
private class MyClass implements BeforeUpdateStaminaListener {
// Make your class implement the listener, and pass in your class as a listener.
public MyClass() {
getStaminaManager().registerBeforeUpdateStaminaListener("myClass", this);
}
@Override
public boolean onBeforeUpdateStamina(String reason, int newStamina) {
// do not intercept this update
return false;
}
@Override
public boolean onBeforeUpdateStamina(String reason, Consumption consumption) {
// Try to intercept if this update is CLIMB_JUMP
if (consumption.consumptionType == ConsumptionType.CLIMB_JUMP) {
return true;
}
// If it is not CLIMB_JUMP, do not intercept.
return false;
}
}
```
### AfterUpdateStaminaListener
```java
import emu.grasscutter.game.managers.StaminaManager.AfterUpdateStaminaListener;
// Listener sample: plugin listens for changes already made.
private class MyClass implements AfterUpdateStaminaListener {
// Make your class implement the listener, and pass in your class as a listener.
public MyClass() {
registerAfterUpdateStaminaListener("myClass", this);
}
@Override
public void onAfterUpdateStamina(String reason, int newStamina) {
// ...
}
}
```
\ No newline at end of file
package emu.grasscutter.game.managers.StaminaManager;
import ch.qos.logback.classic.Logger;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.command.commands.NoStaminaCommand;
import emu.grasscutter.data.GameData;
import emu.grasscutter.game.avatar.Avatar;
import emu.grasscutter.game.entity.EntityAvatar;
import emu.grasscutter.game.entity.GameEntity;
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.game.props.WeaponType;
import emu.grasscutter.net.proto.EntityMoveInfoOuterClass.EntityMoveInfo;
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.Vector;
import emu.grasscutter.net.proto.VehicleInteractTypeOuterClass.VehicleInteractType;
import emu.grasscutter.server.game.GameSession;
import emu.grasscutter.server.packet.send.*;
import emu.grasscutter.utils.Position;
import org.jetbrains.annotations.NotNull;
import java.util.*;
import static emu.grasscutter.Configuration.GAME_OPTIONS;
public class StaminaManager {
// TODO: Skiff state detection?
private final Player player;
private static final HashMap<String, HashSet<MotionState>> MotionStatesCategorized = new HashMap<>() {{
put("CLIMB", new HashSet<>(List.of(
MotionState.MOTION_STATE_CLIMB, // sustained, when not moving no cost no recover
MotionState.MOTION_STATE_STANDBY_TO_CLIMB // NOT OBSERVED, see MOTION_JUMP_UP_WALL_FOR_STANDBY
)));
put("DASH", new HashSet<>(List.of(
MotionState.MOTION_STATE_DANGER_DASH, // sustained
MotionState.MOTION_STATE_DASH // sustained
)));
put("FLY", new HashSet<>(List.of(
MotionState.MOTION_STATE_FLY, // sustained
MotionState.MOTION_STATE_FLY_FAST, // sustained
MotionState.MOTION_STATE_FLY_SLOW, // sustained
MotionState.MOTION_STATE_POWERED_FLY // sustained, recover
)));
put("RUN", new HashSet<>(List.of(
MotionState.MOTION_STATE_DANGER_RUN, // sustained, recover
MotionState.MOTION_STATE_RUN // sustained, recover
)));
put("SKIFF", new HashSet<>(List.of(
MotionState.MOTION_STATE_SKIFF_BOARDING, // NOT OBSERVED even when boarding
MotionState.MOTION_STATE_SKIFF_DASH, // sustained, observed with waverider entity ID.
MotionState.MOTION_STATE_SKIFF_NORMAL, // sustained, OBSERVED when both normal and dashing
MotionState.MOTION_STATE_SKIFF_POWERED_DASH // sustained, recover
)));
put("STANDBY", new HashSet<>(List.of(
MotionState.MOTION_STATE_DANGER_STANDBY_MOVE, // sustained, recover
MotionState.MOTION_STATE_DANGER_STANDBY, // sustained, recover
MotionState.MOTION_STATE_LADDER_TO_STANDBY, // NOT OBSERVED
MotionState.MOTION_STATE_STANDBY_MOVE, // sustained, recover
MotionState.MOTION_STATE_STANDBY // sustained, recover
)));
put("SWIM", new HashSet<>(List.of(
MotionState.MOTION_STATE_SWIM_IDLE, // sustained
MotionState.MOTION_STATE_SWIM_DASH, // immediate and sustained
MotionState.MOTION_STATE_SWIM_JUMP, // NOT OBSERVED
MotionState.MOTION_STATE_SWIM_MOVE // sustained
)));
put("WALK", new HashSet<>(List.of(
MotionState.MOTION_STATE_DANGER_WALK, // sustained, recover
MotionState.MOTION_STATE_WALK // sustained, recover
)));
put("OTHER", new HashSet<>(List.of(
MotionState.MOTION_STATE_CLIMB_JUMP, // cost only once if repeated without switching state
MotionState.MOTION_STATE_DASH_BEFORE_SHAKE, // immediate one time sprint charge.
MotionState.MOTION_STATE_FIGHT, // immediate, if sustained then subsequent will be MOTION_NOTIFY
MotionState.MOTION_STATE_JUMP_UP_WALL_FOR_STANDBY, // immediate, observed when RUN/WALK->CLIMB
MotionState.MOTION_STATE_NOTIFY, // can be either cost or recover - check previous state and check skill casting
MotionState.MOTION_STATE_SIT_IDLE, // sustained, recover
MotionState.MOTION_STATE_JUMP // recover
)));
put("NOCOST_NORECOVER", new HashSet<>(List.of(
MotionState.MOTION_STATE_LADDER_SLIP, // NOT OBSERVED
MotionState.MOTION_STATE_SLIP, // sustained, no cost no recover
MotionState.MOTION_STATE_FLY_IDLE // NOT OBSERVED
)));
put("IGNORE", new HashSet<>(List.of(
// these states have no impact on stamina
MotionState.MOTION_STATE_CROUCH_IDLE,
MotionState.MOTION_STATE_CROUCH_MOVE,
MotionState.MOTION_STATE_CROUCH_ROLL,
MotionState.MOTION_STATE_DESTROY_VEHICLE,
MotionState.MOTION_STATE_FALL_ON_GROUND,
MotionState.MOTION_STATE_FOLLOW_ROUTE,
MotionState.MOTION_STATE_FORCE_SET_POS,
MotionState.MOTION_STATE_GO_UPSTAIRS,
MotionState.MOTION_STATE_JUMP_OFF_WALL,
MotionState.MOTION_STATE_LADDER_IDLE,
MotionState.MOTION_STATE_LADDER_MOVE,
MotionState.MOTION_STATE_LAND_SPEED,
MotionState.MOTION_STATE_MOVE_FAIL_ACK,
MotionState.MOTION_STATE_NONE,
MotionState.MOTION_STATE_NUM,
MotionState.MOTION_STATE_QUEST_FORCE_DRAG,
MotionState.MOTION_STATE_RESET,
MotionState.MOTION_STATE_STANDBY_TO_LADDER,
MotionState.MOTION_STATE_WATERFALL
)));
}};
private final Logger logger = Grasscutter.getLogger();
public final static int GlobalCharacterMaximumStamina = 24000;
public final static int GlobalVehicleMaxStamina = 24000;
private Position currentCoordinates = new Position(0, 0, 0);
private Position previousCoordinates = new Position(0, 0, 0);
private MotionState currentState = MotionState.MOTION_STATE_STANDBY;
private MotionState previousState = MotionState.MOTION_STATE_STANDBY;
private Timer sustainedStaminaHandlerTimer;
private GameSession cachedSession = null;
private GameEntity cachedEntity = null;
private int staminaRecoverDelay = 0;
private final HashMap<String, BeforeUpdateStaminaListener> beforeUpdateStaminaListeners = new HashMap<>();
private final HashMap<String, AfterUpdateStaminaListener> afterUpdateStaminaListeners = new HashMap<>();
private int lastSkillId = 0;
private int lastSkillCasterId = 0;
private boolean lastSkillFirstTick = true;
private int vehicleId = -1;
private int vehicleStamina = GlobalVehicleMaxStamina;
private static final HashSet<Integer> TalentMovements = new HashSet<>(List.of(
10013, 10413
));
private static final HashMap<Integer, Float> ClimbFoodReductionMap = new HashMap<>() {{
// TODO: get real food id
put(0, 0.8f); // Sample food
}};
private static final HashMap<Integer, Float> DashFoodReductionMap = new HashMap<>() {{
// TODO: get real food id
put(0, 0.8f); // Sample food
}};
private static final HashMap<Integer, Float> FlyFoodReductionMap = new HashMap<>() {{
// TODO: get real food id
put(0, 0.8f); // Sample food
}};
private static final HashMap<Integer, Float> SwimFoodReductionMap = new HashMap<>() {{
// TODO: get real food id
put(0, 0.8f); // Sample food
}};
private static final HashMap<Integer, Float> ClimbTalentReductionMap = new HashMap<>() {{
put(262301, 0.8f);
}};
private static final HashMap<Integer, Float> FlyTalentReductionMap = new HashMap<>() {{
put(212301, 0.8f);
put(222301, 0.8f);
}};
private static final HashMap<Integer, Float> SwimTalentReductionMap = new HashMap<>() {{
put(242301, 0.8f);
put(542301, 0.8f);
}};
public static void initialize() {
// TODO: Initialize foods etc.
}
public StaminaManager(Player player) {
this.player = player;
}
// Accessors
public void setSkillCast(int skillId, int skillCasterId) {
lastSkillFirstTick = true;
lastSkillId = skillId;
lastSkillCasterId = skillCasterId;
}
public int getMaxCharacterStamina() {
return player.getProperty(PlayerProperty.PROP_MAX_STAMINA);
}
public int getCurrentCharacterStamina() {
return player.getProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA);
}
public int getMaxVehicleStamina() {
return GlobalVehicleMaxStamina;
}
public int getCurrentVehicleStamina() {
return vehicleStamina;
}
public boolean registerBeforeUpdateStaminaListener(String listenerName, BeforeUpdateStaminaListener listener) {
if (beforeUpdateStaminaListeners.containsKey(listenerName)) {
return false;
}
beforeUpdateStaminaListeners.put(listenerName, listener);
return true;
}
public boolean unregisterBeforeUpdateStaminaListener(String listenerName) {
if (!beforeUpdateStaminaListeners.containsKey(listenerName)) {
return false;
}
beforeUpdateStaminaListeners.remove(listenerName);
return true;
}
public boolean registerAfterUpdateStaminaListener(String listenerName, AfterUpdateStaminaListener listener) {
if (afterUpdateStaminaListeners.containsKey(listenerName)) {
return false;
}
afterUpdateStaminaListeners.put(listenerName, listener);
return true;
}
public boolean unregisterAfterUpdateStaminaListener(String listenerName) {
if (!afterUpdateStaminaListeners.containsKey(listenerName)) {
return false;
}
afterUpdateStaminaListeners.remove(listenerName);
return true;
}
private boolean isPlayerMoving() {
float diffX = currentCoordinates.getX() - previousCoordinates.getX();
float diffY = currentCoordinates.getY() - previousCoordinates.getY();
float diffZ = currentCoordinates.getZ() - previousCoordinates.getZ();
logger.trace("isPlayerMoving: " + previousCoordinates + ", " + currentCoordinates +
", " + diffX + ", " + diffY + ", " + diffZ);
return Math.abs(diffX) > 0.3 || Math.abs(diffY) > 0.2 || Math.abs(diffZ) > 0.3;
}
public int updateStaminaRelative(GameSession session, Consumption consumption, boolean isCharacterStamina) {
int currentStamina = isCharacterStamina ? getCurrentCharacterStamina() : getCurrentVehicleStamina();
if (consumption.amount == 0) {
return currentStamina;
}
// notify will update
for (Map.Entry<String, BeforeUpdateStaminaListener> listener : beforeUpdateStaminaListeners.entrySet()) {
Consumption overriddenConsumption = listener.getValue().onBeforeUpdateStamina(consumption.type.toString(), consumption, isCharacterStamina);
if ((overriddenConsumption.type != consumption.type) && (overriddenConsumption.amount != consumption.amount)) {
logger.debug("Stamina update relative(" +
consumption.type.toString() + ", " + consumption.amount + ") overridden to relative(" +
consumption.type.toString() + ", " + consumption.amount + ") by: " + listener.getKey());
return currentStamina;
}
}
int maxStamina = isCharacterStamina ? getMaxCharacterStamina() : getMaxVehicleStamina();
logger.trace((isCharacterStamina ? "C " : "V ") + currentStamina + "/" + maxStamina + "\t" + currentState + "\t" +
(isPlayerMoving() ? "moving" : " ") + "\t(" + consumption.type + "," +
consumption.amount + ")");
int newStamina = currentStamina + consumption.amount;
if (newStamina < 0) {
newStamina = 0;
} else if (newStamina > maxStamina) {
newStamina = maxStamina;
}
return setStamina(session, consumption.type.toString(), newStamina, isCharacterStamina);
}
public int updateStaminaAbsolute(GameSession session, String reason, int newStamina, boolean isCharacterStamina) {
int currentStamina = isCharacterStamina ? getCurrentCharacterStamina() : getCurrentVehicleStamina();
// notify will update
for (Map.Entry<String, BeforeUpdateStaminaListener> listener : beforeUpdateStaminaListeners.entrySet()) {
int overriddenNewStamina = listener.getValue().onBeforeUpdateStamina(reason, newStamina, isCharacterStamina);
if (overriddenNewStamina != newStamina) {
logger.debug("Stamina update absolute(" +
reason + ", " + newStamina + ") overridden to absolute(" +
reason + ", " + newStamina + ") by: " + listener.getKey());
return currentStamina;
}
}
int maxStamina = isCharacterStamina ? getMaxCharacterStamina() : getMaxVehicleStamina();
if (newStamina < 0) {
newStamina = 0;
} else if (newStamina > maxStamina) {
newStamina = maxStamina;
}
return setStamina(session, reason, newStamina, isCharacterStamina);
}
// Returns new stamina and sends PlayerPropNotify or VehicleStaminaNotify
public int setStamina(GameSession session, String reason, int newStamina, boolean isCharacterStamina) {
// Target Player
if (!GAME_OPTIONS.staminaUsage || session.getPlayer().getStamina()) {
newStamina = getMaxCharacterStamina();
}
// set stamina if is character stamina
if (isCharacterStamina) {
player.setProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA, newStamina);
session.send(new PacketPlayerPropNotify(player, PlayerProperty.PROP_CUR_PERSIST_STAMINA));
} else {
vehicleStamina = newStamina;
session.send(new PacketVehicleStaminaNotify(vehicleId, ((float) newStamina) / 100));
}
// notify updated
for (Map.Entry<String, AfterUpdateStaminaListener> listener : afterUpdateStaminaListeners.entrySet()) {
listener.getValue().onAfterUpdateStamina(reason, newStamina, isCharacterStamina);
}
return newStamina;
}
// 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() && sustainedStaminaHandlerTimer == null) {
sustainedStaminaHandlerTimer = new Timer();
sustainedStaminaHandlerTimer.scheduleAtFixedRate(new SustainedStaminaHandler(), 0, 200);
logger.debug("[MovementManager] SustainedStaminaHandlerTimer started");
}
}
public void stopSustainedStaminaHandler() {
if (sustainedStaminaHandlerTimer != null) {
sustainedStaminaHandlerTimer.cancel();
sustainedStaminaHandlerTimer = null;
logger.debug("[MovementManager] SustainedStaminaHandlerTimer stopped");
}
}
// Handlers
// External trigger handler
public void handleEvtDoSkillSuccNotify(GameSession session, int skillId, int casterId) {
// Ignore if skill not cast by not current active avatar
if (casterId != player.getTeamManager().getCurrentAvatarEntity().getId()) {
return;
}
setSkillCast(skillId, casterId);
// Handle immediate stamina cost
Avatar currentAvatar = player.getTeamManager().getCurrentAvatarEntity().getAvatar();
if (currentAvatar.getAvatarData().getWeaponType() == WeaponType.WEAPON_CLAYMORE) {
// Exclude claymore as their stamina cost starts when MixinStaminaCost gets in
return;
}
// TODO: Differentiate normal attacks from charged attacks and exclude
// TODO: Temporary: Exclude non-claymore attacks for now
/*
if (BowAvatars.contains(currentAvatarId)
|| SwordAvatars.contains(currentAvatarId)
|| PolearmAvatars.contains(currentAvatarId)
|| CatalystAvatars.contains(currentAvatarId)
) {
return;
}
*/
//handleImmediateStamina(session, skillId);
}
public void handleMixinCostStamina(boolean isSwim) {
// Talent moving and claymore avatar charged attack duration
// logger.trace("abilityMixinCostStamina: isSwim: " + isSwim + "\tlastSkill: " + lastSkillId);
if (lastSkillCasterId == player.getTeamManager().getCurrentAvatarEntity().getId()) {
handleImmediateStamina(cachedSession, lastSkillId);
}
}
public void handleCombatInvocationsNotify(@NotNull GameSession session, @NotNull EntityMoveInfo moveInfo, @NotNull GameEntity entity) {
// cache info for later use in SustainedStaminaHandler tick
cachedSession = session;
cachedEntity = entity;
MotionInfo motionInfo = moveInfo.getMotionInfo();
MotionState motionState = motionInfo.getState();
int notifyEntityId = entity.getId();
int currentAvatarEntityId = session.getPlayer().getTeamManager().getCurrentAvatarEntity().getId();
if (notifyEntityId != currentAvatarEntityId && notifyEntityId != vehicleId) {
return;
}
currentState = motionState;
// logger.trace(currentState + "\t" + (notifyEntityId == currentAvatarEntityId ? "character" : "vehicle"));
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, motionState);
}
public void handleVehicleInteractReq(GameSession session, int vehicleId, VehicleInteractType vehicleInteractType) {
if (vehicleInteractType == VehicleInteractType.VEHICLE_INTERACT_TYPE_IN) {
this.vehicleId = vehicleId;
// Reset character stamina here to prevent falling into water immediately on ejection if char stamina is
// close to empty when boarding.
updateStaminaAbsolute(session, "board vehicle", getMaxCharacterStamina(), true);
updateStaminaAbsolute(session, "board vehicle", getMaxVehicleStamina(), false);
} else {
this.vehicleId = -1;
}
}
// Internal handler
private void handleImmediateStamina(GameSession session, @NotNull MotionState motionState) {
switch (motionState) {
case MOTION_STATE_CLIMB:
if (currentState != MotionState.MOTION_STATE_CLIMB) {
updateStaminaRelative(session, new Consumption(ConsumptionType.CLIMB_START), true);
}
break;
case MOTION_STATE_DASH_BEFORE_SHAKE:
if (previousState != MotionState.MOTION_STATE_DASH_BEFORE_SHAKE) {
updateStaminaRelative(session, new Consumption(ConsumptionType.SPRINT), true);
}
break;
case MOTION_STATE_CLIMB_JUMP:
if (previousState != MotionState.MOTION_STATE_CLIMB_JUMP) {
updateStaminaRelative(session, new Consumption(ConsumptionType.CLIMB_JUMP), true);
}
break;
case MOTION_STATE_SWIM_DASH:
if (previousState != MotionState.MOTION_STATE_SWIM_DASH) {
updateStaminaRelative(session, new Consumption(ConsumptionType.SWIM_DASH_START), true);
}
break;
}
}
private void handleImmediateStamina(GameSession session, int skillId) {
Consumption consumption = getFightConsumption(skillId);
updateStaminaRelative(session, consumption, true);
}
private class SustainedStaminaHandler extends TimerTask {
public void run() {
boolean moving = isPlayerMoving();
int currentCharacterStamina = getCurrentCharacterStamina();
int maxCharacterStamina = getMaxCharacterStamina();
int currentVehicleStamina = getCurrentVehicleStamina();
int maxVehicleStamina = getMaxVehicleStamina();
if (moving || (currentCharacterStamina < maxCharacterStamina) || (currentVehicleStamina < maxVehicleStamina)) {
logger.trace("Player moving: " + moving + ", stamina full: " +
(currentCharacterStamina >= maxCharacterStamina) + ", recalculate stamina");
boolean isCharacterStamina = true;
Consumption consumption;
if (MotionStatesCategorized.get("CLIMB").contains(currentState)) {
consumption = getClimbConsumption();
} else if (MotionStatesCategorized.get("DASH").contains(currentState)) {
consumption = getDashConsumption();
} else if (MotionStatesCategorized.get("FLY").contains(currentState)) {
consumption = getFlyConsumption();
} else if (MotionStatesCategorized.get("RUN").contains(currentState)) {
consumption = new Consumption(ConsumptionType.RUN);
} else if (MotionStatesCategorized.get("SKIFF").contains(currentState)) {
consumption = getSkiffConsumption();
isCharacterStamina = false;
} else if (MotionStatesCategorized.get("STANDBY").contains(currentState)) {
consumption = new Consumption(ConsumptionType.STANDBY);
} else if (MotionStatesCategorized.get("SWIM").contains(currentState)) {
consumption = getSwimConsumptions();
} else if (MotionStatesCategorized.get("WALK").contains(currentState)) {
consumption = new Consumption(ConsumptionType.WALK);
} else if (MotionStatesCategorized.get("NOCOST_NORECOVER").contains(currentState)) {
consumption = new Consumption();
} else if (MotionStatesCategorized.get("OTHER").contains(currentState)) {
consumption = getOtherConsumptions();
} else { // ignore
return;
}
if (consumption.amount < 0 && isCharacterStamina) {
// Do not apply reduction factor when recovering stamina
if (player.getTeamManager().getTeamResonances().contains(10301)) {
consumption.amount *= 0.85f;
}
}
// Delay 1 seconds before starts recovering stamina
if (consumption.amount != 0 && cachedSession != null) {
if (consumption.amount < 0) {
staminaRecoverDelay = 0;
}
if (consumption.amount > 0
&& consumption.type != ConsumptionType.POWERED_FLY
&& consumption.type != ConsumptionType.POWERED_SKIFF) {
// For POWERED_* recover immediately - things like Amber's gliding exam and skiff challenges may require this.
if (staminaRecoverDelay < 5) {
// For others recover after 1 seconds (5 ticks) - as official server does.
staminaRecoverDelay++;
consumption.amount = 0;
logger.trace("Delaying recovery: " + staminaRecoverDelay);
}
}
updateStaminaRelative(cachedSession, consumption, isCharacterStamina);
}
}
previousState = currentState;
previousCoordinates = new Position(
currentCoordinates.getX(),
currentCoordinates.getY(),
currentCoordinates.getZ()
);
}
}
private void handleDrowning() {
// TODO: fix drowning waverider entity
int stamina = getCurrentCharacterStamina();
if (stamina < 10) {
logger.trace(getCurrentCharacterStamina() + "/" +
getMaxCharacterStamina() + "\t" + currentState);
if (currentState != MotionState.MOTION_STATE_SWIM_IDLE) {
killAvatar(cachedSession, cachedEntity, PlayerDieType.PLAYER_DIE_TYPE_DRAWN);
}
}
}
// Consumption Calculators
// Stamina Consumption Reduction: https://genshin-impact.fandom.com/wiki/Stamina
private Consumption getFightConsumption(int skillCasting) {
// Talent moving
if (TalentMovements.contains(skillCasting)) {
// TODO: recover 1000 if kamisato hits an enemy at the end of dashing
return getTalentMovingSustainedCost(skillCasting);
}
// Bow avatar charged attack
Avatar currentAvatar = player.getTeamManager().getCurrentAvatarEntity().getAvatar();
switch (currentAvatar.getAvatarData().getWeaponType()) {
case WEAPON_BOW:
return getBowSustainedCost(skillCasting);
case WEAPON_CLAYMORE:
return getClaymoreSustainedCost(skillCasting);
case WEAPON_CATALYST:
return getCatalystCost(skillCasting);
case WEAPON_POLE:
return getPolearmCost(skillCasting);
case WEAPON_SWORD_ONE_HAND:
return getSwordCost(skillCasting);
}
return new Consumption();
}
private Consumption getClimbConsumption() {
Consumption consumption = new Consumption();
if (currentState == MotionState.MOTION_STATE_CLIMB && isPlayerMoving()) {
consumption.type = ConsumptionType.CLIMBING;
consumption.amount = ConsumptionType.CLIMBING.amount;
}
// Climbing specific reductions
consumption.amount *= getFoodCostReductionFactor(ClimbFoodReductionMap);
consumption.amount *= getTalentCostReductionFactor(ClimbTalentReductionMap);
return consumption;
}
private Consumption getSwimConsumptions() {
handleDrowning();
Consumption consumption = new Consumption();
if (currentState == MotionState.MOTION_STATE_SWIM_MOVE) {
consumption.type = ConsumptionType.SWIMMING;
consumption.amount = ConsumptionType.SWIMMING.amount;
}
if (currentState == MotionState.MOTION_STATE_SWIM_DASH) {
consumption.type = ConsumptionType.SWIM_DASH;
consumption.amount = ConsumptionType.SWIM_DASH.amount;
}
// Swimming specific reductions
consumption.amount *= getFoodCostReductionFactor(SwimFoodReductionMap);
consumption.amount *= getTalentCostReductionFactor(SwimTalentReductionMap);
return consumption;
}
private Consumption getDashConsumption() {
Consumption consumption = new Consumption();
if (currentState == MotionState.MOTION_STATE_DASH) {
consumption.type = ConsumptionType.DASH;
consumption.amount = ConsumptionType.DASH.amount;
// Dashing specific reductions
consumption.amount *= getFoodCostReductionFactor(DashFoodReductionMap);
}
return consumption;
}
private Consumption getFlyConsumption() {
// POWERED_FLY, e.g. wind tunnel
if (currentState == MotionState.MOTION_STATE_POWERED_FLY) {
return new Consumption(ConsumptionType.POWERED_FLY);
}
Consumption consumption = new Consumption(ConsumptionType.FLY);
// Flying specific reductions
consumption.amount *= getFoodCostReductionFactor(FlyFoodReductionMap);
consumption.amount *= getTalentCostReductionFactor(FlyTalentReductionMap);
return consumption;
}
private Consumption getSkiffConsumption() {
// No known reduction for skiffing.
return switch (currentState) {
case MOTION_STATE_SKIFF_DASH -> new Consumption(ConsumptionType.SKIFF_DASH);
case MOTION_STATE_SKIFF_POWERED_DASH -> new Consumption(ConsumptionType.POWERED_SKIFF);
case MOTION_STATE_SKIFF_NORMAL -> new Consumption(ConsumptionType.SKIFF);
default -> new Consumption();
};
}
private Consumption getOtherConsumptions() {
switch (currentState) {
case MOTION_STATE_NOTIFY:
// if (BowSkills.contains(lastSkillId)) {
// return new Consumption(ConsumptionType.FIGHT, 500);
// }
break;
case MOTION_STATE_FIGHT:
// TODO: what if charged attack
return new Consumption(ConsumptionType.FIGHT, 500);
}
return new Consumption();
}
// Reduction getter
private float getTalentCostReductionFactor(HashMap<Integer, Float> talentReductionMap) {
// All known talents reductions are not stackable
float reduction = 1;
for (EntityAvatar entity : cachedSession.getPlayer().getTeamManager().getActiveTeam()) {
for (int skillId : entity.getAvatar().getProudSkillList()) {
if (talentReductionMap.containsKey(skillId)) {
float potentialLowerReduction = talentReductionMap.get(skillId);
if (potentialLowerReduction < reduction) {
reduction = potentialLowerReduction;
}
}
}
}
return reduction;
}
private float getFoodCostReductionFactor(HashMap<Integer, Float> foodReductionMap) {
// All known food reductions are not stackable
// TODO: Check consumed food (buff?) and return proper factor
float reduction = 1;
return reduction;
}
private Consumption getTalentMovingSustainedCost(int skillId) {
if (lastSkillFirstTick) {
lastSkillFirstTick = false;
return new Consumption(ConsumptionType.TALENT_DASH, -1000);
} else {
return new Consumption(ConsumptionType.TALENT_DASH, -500);
}
}
private Consumption getBowSustainedCost(int skillId) {
// Note that bow skills actually recovers stamina
// Character specific handling
// switch (skillId) {
// // No known bow skills cost stamina
// }
return new Consumption(ConsumptionType.FIGHT, +500);
}
private Consumption getCatalystCost(int skillId) {
Consumption consumption = new Consumption(ConsumptionType.FIGHT, -5000);
// Character specific handling
switch (skillId) {
// TODO:
}
return consumption;
}
private Consumption getClaymoreSustainedCost(int skillId) {
Consumption consumption = new Consumption(ConsumptionType.FIGHT, -1333); // 4000 / 3 = 1333
// Character specific handling
switch (skillId) {
case 10571:
case 10532:
consumption.amount = 0;
break;
case 10160:
if (player.getTeamManager().getCurrentAvatarEntity().getAvatar().getProudSkillList().contains(162101)) {
consumption.amount /= 2;
}
break;
}
return consumption;
}
private Consumption getPolearmCost(int skillId) {
Consumption consumption = new Consumption(ConsumptionType.FIGHT, -2500);
// Character specific handling
switch (skillId) {
// TODO:
}
return consumption;
}
private Consumption getSwordCost(int skillId) {
Consumption consumption = new Consumption(ConsumptionType.FIGHT, -2000);
// Character specific handling
switch (skillId) {
case 10421:
consumption.amount = -2500;
break;
}
return consumption;
}
}
...@@ -21,16 +21,16 @@ public class InvokeHandler<T> { ...@@ -21,16 +21,16 @@ public class InvokeHandler<T> {
public synchronized void addEntry(ForwardType forward, T entry) { public synchronized void addEntry(ForwardType forward, T entry) {
switch (forward) { switch (forward) {
case FORWARD_TO_ALL -> entryListForwardAll.add(entry); case FORWARD_TYPE_TO_ALL -> entryListForwardAll.add(entry);
case FORWARD_TO_ALL_EXCEPT_CUR, FORWARD_TO_ALL_EXIST_EXCEPT_CUR -> entryListForwardAllExceptCur.add(entry); case FORWARD_TYPE_TO_ALL_EXCEPT_CUR, FORWARD_TYPE_TO_ALL_EXIST_EXCEPT_CUR -> entryListForwardAllExceptCur.add(entry);
case FORWARD_TO_HOST -> entryListForwardHost.add(entry); case FORWARD_TYPE_TO_HOST -> entryListForwardHost.add(entry);
default -> { default -> {
} }
} }
} }
public synchronized void update(Player player) { public synchronized void update(Player player) {
if (player.getWorld() == null) { if (player.getWorld() == null || player.getScene() == null) {
this.entryListForwardAll.clear(); this.entryListForwardAll.clear();
this.entryListForwardAllExceptCur.clear(); this.entryListForwardAllExceptCur.clear();
this.entryListForwardHost.clear(); this.entryListForwardHost.clear();
......
...@@ -4,13 +4,17 @@ import dev.morphia.annotations.*; ...@@ -4,13 +4,17 @@ import dev.morphia.annotations.*;
import emu.grasscutter.GameConstants; import emu.grasscutter.GameConstants;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.GameData; import emu.grasscutter.data.GameData;
import emu.grasscutter.data.def.PlayerLevelData; import emu.grasscutter.data.excels.PlayerLevelData;
import emu.grasscutter.database.DatabaseHelper; import emu.grasscutter.database.DatabaseHelper;
import emu.grasscutter.game.Account; import emu.grasscutter.game.Account;
import emu.grasscutter.game.CoopRequest; import emu.grasscutter.game.CoopRequest;
import emu.grasscutter.game.ability.AbilityManager;
import emu.grasscutter.game.avatar.Avatar; import emu.grasscutter.game.avatar.Avatar;
import emu.grasscutter.game.avatar.AvatarProfileData; import emu.grasscutter.game.avatar.AvatarProfileData;
import emu.grasscutter.game.avatar.AvatarStorage; import emu.grasscutter.game.avatar.AvatarStorage;
import emu.grasscutter.game.entity.EntityMonster;
import emu.grasscutter.game.entity.EntityVehicle;
import emu.grasscutter.game.managers.DeforestationManager.DeforestationManager;
import emu.grasscutter.game.entity.EntityGadget; import emu.grasscutter.game.entity.EntityGadget;
import emu.grasscutter.game.entity.EntityItem; import emu.grasscutter.game.entity.EntityItem;
import emu.grasscutter.game.entity.GameEntity; import emu.grasscutter.game.entity.GameEntity;
...@@ -22,12 +26,15 @@ import emu.grasscutter.game.inventory.GameItem; ...@@ -22,12 +26,15 @@ 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.InsectCaptureManager;
import emu.grasscutter.game.managers.SotSManager.SotSManager; import emu.grasscutter.game.managers.StaminaManager.StaminaManager;
import emu.grasscutter.game.managers.SotSManager;
import emu.grasscutter.game.managers.EnergyManager.EnergyManager;
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;
import emu.grasscutter.game.props.SceneType; import emu.grasscutter.game.props.SceneType;
import emu.grasscutter.game.quest.QuestManager;
import emu.grasscutter.game.shop.ShopLimit; import emu.grasscutter.game.shop.ShopLimit;
import emu.grasscutter.game.managers.MapMarkManager.*; import emu.grasscutter.game.managers.MapMarkManager.*;
import emu.grasscutter.game.tower.TowerManager; import emu.grasscutter.game.tower.TowerManager;
...@@ -44,10 +51,12 @@ import emu.grasscutter.net.proto.OnlinePlayerInfoOuterClass.OnlinePlayerInfo; ...@@ -44,10 +51,12 @@ import emu.grasscutter.net.proto.OnlinePlayerInfoOuterClass.OnlinePlayerInfo;
import emu.grasscutter.net.proto.PlayerLocationInfoOuterClass.PlayerLocationInfo; import emu.grasscutter.net.proto.PlayerLocationInfoOuterClass.PlayerLocationInfo;
import emu.grasscutter.net.proto.ProfilePictureOuterClass.ProfilePicture; import emu.grasscutter.net.proto.ProfilePictureOuterClass.ProfilePicture;
import emu.grasscutter.net.proto.SocialDetailOuterClass.SocialDetail; import emu.grasscutter.net.proto.SocialDetailOuterClass.SocialDetail;
import emu.grasscutter.server.event.player.PlayerJoinEvent; import emu.grasscutter.server.event.player.PlayerJoinEvent;
import emu.grasscutter.server.event.player.PlayerQuitEvent; import emu.grasscutter.server.event.player.PlayerQuitEvent;
import emu.grasscutter.server.game.GameServer; import emu.grasscutter.server.game.GameServer;
import emu.grasscutter.server.game.GameSession; import emu.grasscutter.server.game.GameSession;
import emu.grasscutter.server.game.GameSession.SessionState;
import emu.grasscutter.server.packet.send.*; import emu.grasscutter.server.packet.send.*;
import emu.grasscutter.utils.DateHelper; import emu.grasscutter.utils.DateHelper;
import emu.grasscutter.utils.Position; import emu.grasscutter.utils.Position;
...@@ -59,12 +68,11 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; ...@@ -59,12 +68,11 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import java.util.*; import java.util.*;
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedBlockingQueue;
import static emu.grasscutter.Configuration.*;
@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;
...@@ -76,11 +84,18 @@ public class Player { ...@@ -76,11 +84,18 @@ public class Player {
private Position pos; private Position pos;
private Position rotation; private Position rotation;
private PlayerBirthday birthday; private PlayerBirthday birthday;
private PlayerCodex codex;
private Map<Integer, Integer> properties; private Map<Integer, Integer> properties;
private Set<Integer> nameCardList; private Set<Integer> nameCardList;
private Set<Integer> flyCloakList; private Set<Integer> flyCloakList;
private Set<Integer> costumeList; private Set<Integer> costumeList;
private Set<Integer> unlockedForgingBlueprints;
private Integer widgetId;
private Set<Integer> realmList;
private Integer currentRealmId;
@Transient private long nextGuid = 0; @Transient private long nextGuid = 0;
@Transient private int peerId; @Transient private int peerId;
...@@ -92,8 +107,11 @@ public class Player { ...@@ -92,8 +107,11 @@ public class Player {
@Transient private FriendsList friendsList; @Transient private FriendsList friendsList;
@Transient private MailHandler mailHandler; @Transient private MailHandler mailHandler;
@Transient private MessageHandler messageHandler; @Transient private MessageHandler messageHandler;
@Transient private AbilityManager abilityManager;
@Transient private QuestManager questManager;
@Transient private SotSManager sotsManager; @Transient private SotSManager sotsManager;
@Transient private InsectCaptureManager insectCaptureManager;
private TeamManager teamManager; private TeamManager teamManager;
...@@ -111,6 +129,7 @@ public class Player { ...@@ -111,6 +129,7 @@ public class Player {
private int mainCharacterId; private int mainCharacterId;
private boolean godmode; private boolean godmode;
private boolean stamina;
private boolean moonCard; private boolean moonCard;
private Date moonCardStartTime; private Date moonCardStartTime;
private int moonCardDuration; private int moonCardDuration;
...@@ -131,11 +150,13 @@ public class Player { ...@@ -131,11 +150,13 @@ public class Player {
@Transient private final InvokeHandler<AbilityInvokeEntry> abilityInvokeHandler; @Transient private final InvokeHandler<AbilityInvokeEntry> abilityInvokeHandler;
@Transient private final InvokeHandler<AbilityInvokeEntry> clientAbilityInitFinishHandler; @Transient private final InvokeHandler<AbilityInvokeEntry> clientAbilityInitFinishHandler;
private MapMarksManager mapMarksManager; @Transient private MapMarksManager mapMarksManager;
@Transient private MovementManager movementManager; @Transient private StaminaManager staminaManager;
@Transient private EnergyManager energyManager;
@Transient private DeforestationManager deforestationManager;
private long springLastUsed; private long springLastUsed;
private HashMap<String, MapMark> mapMarks;
@Deprecated @Deprecated
@SuppressWarnings({"rawtypes", "unchecked"}) // Morphia only! @SuppressWarnings({"rawtypes", "unchecked"}) // Morphia only!
...@@ -145,6 +166,11 @@ public class Player { ...@@ -145,6 +166,11 @@ public class Player {
this.friendsList = new FriendsList(this); this.friendsList = new FriendsList(this);
this.mailHandler = new MailHandler(this); this.mailHandler = new MailHandler(this);
this.towerManager = new TowerManager(this); this.towerManager = new TowerManager(this);
this.abilityManager = new AbilityManager(this);
this.deforestationManager = new DeforestationManager(this);
this.insectCaptureManager = new InsectCaptureManager(this);
this.setQuestManager(new QuestManager(this));
this.pos = new Position(); this.pos = new Position();
this.rotation = new Position(); this.rotation = new Position();
this.properties = new HashMap<>(); this.properties = new HashMap<>();
...@@ -159,6 +185,7 @@ public class Player { ...@@ -159,6 +185,7 @@ public class Player {
this.nameCardList = new HashSet<>(); this.nameCardList = new HashSet<>();
this.flyCloakList = new HashSet<>(); this.flyCloakList = new HashSet<>();
this.costumeList = new HashSet<>(); this.costumeList = new HashSet<>();
this.unlockedForgingBlueprints = new HashSet<>();
this.setSceneId(3); this.setSceneId(3);
this.setRegionId(1); this.setRegionId(1);
...@@ -173,13 +200,15 @@ public class Player { ...@@ -173,13 +200,15 @@ public class Player {
this.birthday = new PlayerBirthday(); this.birthday = new PlayerBirthday();
this.rewardedLevels = new HashSet<>(); this.rewardedLevels = new HashSet<>();
this.moonCardGetTimes = new HashSet<>(); this.moonCardGetTimes = new HashSet<>();
this.codex = new PlayerCodex(this);
this.shopLimit = new ArrayList<>(); this.shopLimit = new ArrayList<>();
this.expeditionInfo = new HashMap<>(); this.expeditionInfo = new HashMap<>();
this.messageHandler = null; this.messageHandler = null;
this.mapMarksManager = new MapMarksManager(); this.mapMarksManager = new MapMarksManager(this);
this.movementManager = new MovementManager(this); this.staminaManager = new StaminaManager(this);
this.sotsManager = new SotSManager(this); this.sotsManager = new SotSManager(this);
this.energyManager = new EnergyManager(this);
} }
// On player creation // On player creation
...@@ -192,6 +221,7 @@ public class Player { ...@@ -192,6 +221,7 @@ public class Player {
this.signature = ""; this.signature = "";
this.teamManager = new TeamManager(this); this.teamManager = new TeamManager(this);
this.birthday = new PlayerBirthday(); this.birthday = new PlayerBirthday();
this.codex = new PlayerCodex(this);
this.setProperty(PlayerProperty.PROP_PLAYER_LEVEL, 1); this.setProperty(PlayerProperty.PROP_PLAYER_LEVEL, 1);
this.setProperty(PlayerProperty.PROP_IS_SPRING_AUTO_USE, 1); this.setProperty(PlayerProperty.PROP_IS_SPRING_AUTO_USE, 1);
this.setProperty(PlayerProperty.PROP_SPRING_AUTO_USE_PERCENT, 50); this.setProperty(PlayerProperty.PROP_SPRING_AUTO_USE_PERCENT, 50);
...@@ -205,9 +235,11 @@ public class Player { ...@@ -205,9 +235,11 @@ public class Player {
this.getPos().set(GameConstants.START_POSITION); this.getPos().set(GameConstants.START_POSITION);
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);
this.movementManager = new MovementManager(this); this.staminaManager = new StaminaManager(this);
this.sotsManager = new SotSManager(this); this.sotsManager = new SotSManager(this);
this.energyManager = new EnergyManager(this);
this.deforestationManager = new DeforestationManager(this);
} }
public int getUid() { public int getUid() {
...@@ -229,7 +261,6 @@ public class Player { ...@@ -229,7 +261,6 @@ public class Player {
public void setAccount(Account account) { public void setAccount(Account account) {
this.account = account; this.account = account;
this.account.setPlayerId(getUid());
} }
public GameSession getSession() { public GameSession getSession() {
...@@ -295,6 +326,39 @@ public class Player { ...@@ -295,6 +326,39 @@ public class Player {
this.updateProfile(); this.updateProfile();
} }
public Integer getWidgetId() {
return widgetId;
}
public void setWidgetId(Integer widgetId) {
this.widgetId = widgetId;
}
public Set<Integer> getRealmList() {
return realmList;
}
public void setRealmList(Set<Integer> realmList) {
this.realmList = realmList;
}
public void addRealmList(int realmId) {
if (this.realmList == null) {
this.realmList = new HashSet<>();
} else if (this.realmList.contains(realmId)) {
return;
}
this.realmList.add(realmId);
}
public Integer getCurrentRealmId() {
return currentRealmId;
}
public void setCurrentRealmId(Integer currentRealmId) {
this.currentRealmId = currentRealmId;
}
public Position getPos() { public Position getPos() {
return pos; return pos;
} }
...@@ -353,7 +417,7 @@ public class Player { ...@@ -353,7 +417,7 @@ public class Player {
} }
private float getExpModifier() { private float getExpModifier() {
return Grasscutter.getConfig().getGameServerOptions().getGameRates().ADVENTURE_EXP_RATE; return GAME_OPTIONS.rates.adventureExp;
} }
// Affected by exp rate // Affected by exp rate
...@@ -409,6 +473,14 @@ public class Player { ...@@ -409,6 +473,14 @@ public class Player {
return towerManager; return towerManager;
} }
public QuestManager getQuestManager() {
return questManager;
}
public void setQuestManager(QuestManager questManager) {
this.questManager = questManager;
}
public PlayerGachaInfo getGachaInfo() { public PlayerGachaInfo getGachaInfo() {
return gachaInfo; return gachaInfo;
} }
...@@ -446,8 +518,12 @@ public class Player { ...@@ -446,8 +518,12 @@ public class Player {
return this.nameCardList; return this.nameCardList;
} }
public Set<Integer> getUnlockedForgingBlueprints() {
return unlockedForgingBlueprints;
}
public MpSettingType getMpSetting() { public MpSettingType getMpSetting() {
return MpSettingType.MP_SETTING_ENTER_AFTER_APPLY; // TEMP return MpSettingType.MP_SETTING_TYPE_ENTER_AFTER_APPLY; // TEMP
} }
public Queue<AttackResult> getAttackResults() { public Queue<AttackResult> getAttackResults() {
...@@ -699,7 +775,6 @@ public class Player { ...@@ -699,7 +775,6 @@ public class Player {
return expeditionInfo.get(avaterGuid); return expeditionInfo.get(avaterGuid);
} }
public List<ShopLimit> getShopLimit() { public List<ShopLimit> getShopLimit() {
return shopLimit; return shopLimit;
} }
...@@ -727,7 +802,14 @@ public class Player { ...@@ -727,7 +802,14 @@ public class Player {
} }
this.save(); this.save();
} }
public boolean getStamina() {
// Get Stamina
return stamina;
}
public void setStamina(boolean stamina) {
// Set Stamina
this.stamina = stamina;
}
public boolean inGodmode() { public boolean inGodmode() {
return godmode; return godmode;
} }
...@@ -744,7 +826,7 @@ public class Player { ...@@ -744,7 +826,7 @@ public class Player {
this.hasSentAvatarDataNotify = hasSentAvatarDataNotify; this.hasSentAvatarDataNotify = hasSentAvatarDataNotify;
} }
public void addAvatar(Avatar avatar) { public void addAvatar(Avatar avatar, boolean addToCurrentTeam) {
boolean result = getAvatars().addAvatar(avatar); boolean result = getAvatars().addAvatar(avatar);
if (result) { if (result) {
...@@ -755,14 +837,22 @@ public class Player { ...@@ -755,14 +837,22 @@ public class Player {
if (hasSentAvatarDataNotify()) { if (hasSentAvatarDataNotify()) {
// Recalc stats // Recalc stats
avatar.recalcStats(); avatar.recalcStats();
// Packet // Packet, show notice on left if the avatar will be added to the team
sendPacket(new PacketAvatarAddNotify(avatar, false)); sendPacket(new PacketAvatarAddNotify(avatar, addToCurrentTeam && this.getTeamManager().canAddAvatarToCurrentTeam()));
if (addToCurrentTeam) {
// If space in team, add
this.getTeamManager().addAvatarToCurrentTeam(avatar);
}
} }
} else { } else {
// Failed adding avatar // Failed adding avatar
} }
} }
public void addAvatar(Avatar avatar) {
addAvatar(avatar, true);
}
public void addFlycloak(int flycloakId) { public void addFlycloak(int flycloakId) {
this.getFlyCloakList().add(flycloakId); this.getFlyCloakList().add(flycloakId);
this.sendPacket(new PacketAvatarGainFlycloakNotify(flycloakId)); this.sendPacket(new PacketAvatarGainFlycloakNotify(flycloakId));
...@@ -830,62 +920,58 @@ public class Player { ...@@ -830,62 +920,58 @@ public class Player {
public void interactWith(int gadgetEntityId) { public void interactWith(int gadgetEntityId) {
GameEntity entity = getScene().getEntityById(gadgetEntityId); GameEntity entity = getScene().getEntityById(gadgetEntityId);
if (entity == null) { if (entity == null) {
return; return;
} }
// Handle // Handle
if (entity instanceof EntityItem) { if (entity instanceof EntityItem drop) {
// Pick item // Pick item
EntityItem drop = (EntityItem) entity;
if (!drop.isShare()) // check drop owner to avoid someone picked up item in others' world if (!drop.isShare()) // check drop owner to avoid someone picked up item in others' world
{ {
int dropOwner = (int)(drop.getGuid() >> 32); int dropOwner = (int)(drop.getGuid() >> 32);
if (dropOwner != getUid()) if (dropOwner != getUid()) {
return; return;
}
} }
entity.getScene().removeEntity(entity); entity.getScene().removeEntity(entity);
GameItem item = new GameItem(drop.getItemData(), drop.getCount()); GameItem item = new GameItem(drop.getItemData(), drop.getCount());
// Add to inventory // Add to inventory
boolean success = getInventory().addItem(item, ActionReason.SubfieldDrop); boolean success = getInventory().addItem(item, ActionReason.SubfieldDrop);
if (success) { if (success) {
if (!drop.isShare()) { // not shared drop
if (!drop.isShare()) // not shared drop this.sendPacket(new PacketGadgetInteractRsp(drop, InteractType.INTERACT_TYPE_PICK_ITEM));
this.sendPacket(new PacketGadgetInteractRsp(drop, InteractType.INTERACT_PICK_ITEM)); }else{
else this.getScene().broadcastPacket(new PacketGadgetInteractRsp(drop, InteractType.INTERACT_TYPE_PICK_ITEM));
this.getScene().broadcastPacket(new PacketGadgetInteractRsp(drop, InteractType.INTERACT_PICK_ITEM)); }
} }
} else if (entity instanceof EntityGadget) { } else if (entity instanceof EntityGadget gadget) {
EntityGadget gadget = (EntityGadget) entity;
if (gadget.getGadgetData().getType() == EntityType.RewardStatue) { if (gadget.getGadgetData().getType() == EntityType.RewardStatue) {
if (scene.getChallenge() != null) { if (scene.getChallenge() != null) {
scene.getChallenge().getStatueDrops(this); scene.getChallenge().getStatueDrops(this);
} }
this.sendPacket(new PacketGadgetInteractRsp(gadget, InteractType.INTERACT_TYPE_OPEN_STATUE));
this.sendPacket(new PacketGadgetInteractRsp(gadget, InteractType.INTERACT_OPEN_STATUE));
} }
} else if (entity instanceof EntityMonster monster) {
insectCaptureManager.arrestSmallCreature(monster);
} else if (entity instanceof EntityVehicle vehicle) {// try to arrest it, example: glowworm
insectCaptureManager.arrestSmallCreature(vehicle);
} else { } else {
// Delete directly // Delete directly
entity.getScene().removeEntity(entity); entity.getScene().removeEntity(entity);
} }
return;
} }
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) {
if (this.hasSentAvatarDataNotify) { this.getSession().send(packet);
this.getSession().send(packet);
}
} }
public OnlinePlayerInfo getOnlinePlayerInfo() { public OnlinePlayerInfo getOnlinePlayerInfo() {
...@@ -920,6 +1006,8 @@ public class Player { ...@@ -920,6 +1006,8 @@ public class Player {
return this.birthday.getDay() > 0; return this.birthday.getDay() > 0;
} }
public PlayerCodex getCodex(){ return this.codex; }
public Set<Integer> getRewardedLevels() { public Set<Integer> getRewardedLevels() {
return rewardedLevels; return rewardedLevels;
} }
...@@ -944,8 +1032,8 @@ public class Player { ...@@ -944,8 +1032,8 @@ public class Player {
} }
} }
} else { } else {
List<Integer> showAvatarList = DatabaseHelper.getPlayerById(id).getShowAvatarList(); List<Integer> showAvatarList = DatabaseHelper.getPlayerByUid(id).getShowAvatarList();
AvatarStorage avatars = DatabaseHelper.getPlayerById(id).getAvatars(); AvatarStorage avatars = DatabaseHelper.getPlayerByUid(id).getAvatars();
avatars.loadFromDatabase(); avatars.loadFromDatabase();
if (showAvatarList != null) { if (showAvatarList != null) {
for (int avatarId : showAvatarList) { for (int avatarId : showAvatarList) {
...@@ -985,7 +1073,7 @@ public class Player { ...@@ -985,7 +1073,7 @@ public class Player {
player = this; player = this;
shouldRecalc = false; shouldRecalc = false;
} else { } else {
player = DatabaseHelper.getPlayerById(id); player = DatabaseHelper.getPlayerByUid(id);
player.getAvatars().loadFromDatabase(); player.getAvatars().loadFromDatabase();
player.getInventory().loadFromDatabase(); player.getInventory().loadFromDatabase();
shouldRecalc = true; shouldRecalc = true;
...@@ -1024,10 +1112,26 @@ public class Player { ...@@ -1024,10 +1112,26 @@ 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; }
public EnergyManager getEnergyManager() {
return this.energyManager;
}
public AbilityManager getAbilityManager() {
return abilityManager;
}
public DeforestationManager getDeforestationManager() {
return deforestationManager;
}
public HashMap<String, MapMark> getMapMarks() { return mapMarks; }
public void setMapMarks(HashMap<String, MapMark> newMarks) { mapMarks = newMarks; }
public synchronized void onTick() { public synchronized void onTick() {
// Check ping // Check ping
if (this.getLastPingTime() > System.currentTimeMillis() + 60000) { if (this.getLastPingTime() > System.currentTimeMillis() + 60000) {
...@@ -1039,7 +1143,10 @@ public class Player { ...@@ -1039,7 +1143,10 @@ public class Player {
while (it.hasNext()) { while (it.hasNext()) {
CoopRequest req = it.next(); CoopRequest req = it.next();
if (req.isExpired()) { if (req.isExpired()) {
req.getRequester().sendPacket(new PacketPlayerApplyEnterMpResultNotify(this, false, PlayerApplyEnterMpResultNotifyOuterClass.PlayerApplyEnterMpResultNotify.Reason.SYSTEM_JUDGE)); req.getRequester().sendPacket(new PacketPlayerApplyEnterMpResultNotify(
this,
false,
PlayerApplyEnterMpResultNotifyOuterClass.PlayerApplyEnterMpResultNotify.Reason.REASON_SYSTEM_JUDGE));
it.remove(); it.remove();
} }
} }
...@@ -1083,6 +1190,7 @@ public class Player { ...@@ -1083,6 +1190,7 @@ public class Player {
@PostLoad @PostLoad
private void onLoad() { private void onLoad() {
this.getCodex().setPlayer(this);
this.getTeamManager().setPlayer(this); this.getTeamManager().setPlayer(this);
this.getTowerManager().setPlayer(this); this.getTowerManager().setPlayer(this);
} }
...@@ -1090,23 +1198,20 @@ public class Player { ...@@ -1090,23 +1198,20 @@ public class Player {
public void save() { public void save() {
DatabaseHelper.savePlayer(this); DatabaseHelper.savePlayer(this);
} }
public void onLogin() { // Called from tokenrsp
public void loadFromDatabase() {
// Make sure these exist // Make sure these exist
if (this.getTeamManager() == null) { if (this.getTeamManager() == null) {
this.teamManager = new TeamManager(this); this.teamManager = new TeamManager(this);
} }
if (this.getCodex() == null) {
this.codex = new PlayerCodex(this);
}
if (this.getProfile().getUid() == 0) { if (this.getProfile().getUid() == 0) {
this.getProfile().syncWithCharacter(this); this.getProfile().syncWithCharacter(this);
} }
// Check if player object exists in server
// TODO - optimize
Player exists = this.getServer().getPlayerByUid(getUid());
if (exists != null) {
exists.getSession().close();
}
// Load from db // Load from db
this.getAvatars().loadFromDatabase(); this.getAvatars().loadFromDatabase();
this.getInventory().loadFromDatabase(); this.getInventory().loadFromDatabase();
...@@ -1114,17 +1219,36 @@ public class Player { ...@@ -1114,17 +1219,36 @@ public class Player {
this.getFriendsList().loadFromDatabase(); this.getFriendsList().loadFromDatabase();
this.getMailHandler().loadFromDatabase(); this.getMailHandler().loadFromDatabase();
this.getQuestManager().loadFromDatabase();
// Create world // Add to gameserver (Always handle last)
World world = new World(this);
world.addPlayer(this);
// Add to gameserver
if (getSession().isActive()) { if (getSession().isActive()) {
getServer().registerPlayer(this); getServer().registerPlayer(this);
getProfile().setPlayer(this); // Set online getProfile().setPlayer(this); // Set online
} }
}
public void onLogin() {
// Quest - Commented out because a problem is caused if you log out while this quest is active
/*
if (getQuestManager().getMainQuestById(351) == null) {
GameQuest quest = getQuestManager().addQuest(35104);
if (quest != null) {
quest.finish();
}
getQuestManager().addQuest(35101);
this.setSceneId(3);
this.getPos().set(GameConstants.START_POSITION);
}
*/
// Create world
World world = new World(this);
world.addPlayer(this);
// Multiplayer setting // Multiplayer setting
this.setProperty(PlayerProperty.PROP_PLAYER_MP_SETTING_TYPE, this.getMpSetting().getNumber()); this.setProperty(PlayerProperty.PROP_PLAYER_MP_SETTING_TYPE, this.getMpSetting().getNumber());
this.setProperty(PlayerProperty.PROP_IS_MP_MODE_AVAILABLE, 1); this.setProperty(PlayerProperty.PROP_IS_MP_MODE_AVAILABLE, 1);
...@@ -1134,6 +1258,14 @@ public class Player { ...@@ -1134,6 +1258,14 @@ public class Player {
session.send(new PacketStoreWeightLimitNotify()); session.send(new PacketStoreWeightLimitNotify());
session.send(new PacketPlayerStoreNotify(this)); session.send(new PacketPlayerStoreNotify(this));
session.send(new PacketAvatarDataNotify(this)); session.send(new PacketAvatarDataNotify(this));
session.send(new PacketFinishedParentQuestNotify(this));
session.send(new PacketQuestListNotify(this));
session.send(new PacketCodexDataFullNotify(this));
session.send(new PacketAllWidgetDataNotify(this));
session.send(new PacketWidgetGadgetAllDataNotify());
session.send(new PacketPlayerHomeCompInfoNotify(this));
session.send(new PacketHomeComfortInfoNotify(this));
session.send(new PacketForgeDataNotify(this));
getTodayMoonCard(); // The timer works at 0:0, some users log in after that, use this method to check if they have received a reward today or not. If not, send the reward. getTodayMoonCard(); // The timer works at 0:0, some users log in after that, use this method to check if they have received a reward today or not. If not, send the reward.
...@@ -1143,6 +1275,9 @@ public class Player { ...@@ -1143,6 +1275,9 @@ public class Player {
// First notify packets sent // First notify packets sent
this.setHasSentAvatarDataNotify(true); this.setHasSentAvatarDataNotify(true);
// Set session state
session.setState(SessionState.ACTIVE);
// Call join event. // Call join event.
PlayerJoinEvent event = new PlayerJoinEvent(this); event.call(); PlayerJoinEvent event = new PlayerJoinEvent(this); event.call();
...@@ -1151,31 +1286,48 @@ public class Player { ...@@ -1151,31 +1286,48 @@ public class Player {
} }
public void onLogout() { public void onLogout() {
// stop stamina calculation try{
getMovementManager().resetTimer(); // stop stamina calculation
getStaminaManager().stopSustainedStaminaHandler();
// force to leave the dungeon // force to leave the dungeon (inside has a "if")
if (getScene().getSceneType() == SceneType.SCENE_DUNGEON) {
this.getServer().getDungeonManager().exitDungeon(this); this.getServer().getDungeonManager().exitDungeon(this);
}
// Leave world
if (this.getWorld() != null) {
this.getWorld().removePlayer(this);
}
// Status stuff // Leave world
this.getProfile().syncWithCharacter(this); if (this.getWorld() != null) {
this.getProfile().setPlayer(null); // Set offline this.getWorld().removePlayer(this);
}
// Status stuff
this.getProfile().syncWithCharacter(this);
this.getProfile().setPlayer(null); // Set offline
this.getCoopRequests().clear(); this.getCoopRequests().clear();
// Save to db // Save to db
this.save(); this.save();
this.getTeamManager().saveAvatars(); this.getTeamManager().saveAvatars();
this.getFriendsList().save(); this.getFriendsList().save();
// Call quit event. // Call quit event.
PlayerQuitEvent event = new PlayerQuitEvent(this); event.call(); PlayerQuitEvent event = new PlayerQuitEvent(this); event.call();
//reset wood
getDeforestationManager().resetWood();
}catch (Throwable e){
e.printStackTrace();
Grasscutter.getLogger().warn("Player (UID {}) save failure", getUid());
}finally {
removeFromServer();
}
}
public void removeFromServer() {
// Remove from server.
//Note: DON'T DELETE BY UID,BECAUSE THERE ARE MULTIPLE SAME UID PLAYERS WHEN DUPLICATED LOGIN!
//so I decide to delete by object rather than uid
getServer().getPlayers().values().removeIf(player1 -> player1 == this);
} }
public enum SceneLoadState { public enum SceneLoadState {
...@@ -1214,7 +1366,7 @@ public class Player { ...@@ -1214,7 +1366,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 <= SotSManager.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 +1383,7 @@ public class Player { ...@@ -1231,7 +1383,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 <= StaminaManager.GlobalCharacterMaximumStamina)) { 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 +1394,7 @@ public class Player { ...@@ -1242,7 +1394,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
......
package emu.grasscutter.game.player;
import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Transient;
import emu.grasscutter.data.GameData;
import emu.grasscutter.data.excels.CodexAnimalData;
import emu.grasscutter.data.excels.CodexReliquaryData;
import emu.grasscutter.game.entity.EntityMonster;
import emu.grasscutter.game.entity.GameEntity;
import emu.grasscutter.game.inventory.GameItem;
import emu.grasscutter.game.inventory.ItemType;
import emu.grasscutter.game.inventory.MaterialType;
import emu.grasscutter.server.packet.send.PacketCodexDataUpdateNotify;
import java.util.*;
@Entity
public class PlayerCodex {
@Transient private Player player;
//itemId is not codexId!
private Set<Integer> unlockedWeapon;
private Map<Integer, Integer> unlockedAnimal;
private Set<Integer> unlockedMaterial;
private Set<Integer> unlockedBook;
private Set<Integer> unlockedTip;
private Set<Integer> unlockedView;
private Set<Integer> unlockedReliquary;
private Set<Integer> unlockedReliquarySuitCodex;
public PlayerCodex(){
this.unlockedWeapon = new HashSet<>();
this.unlockedAnimal = new HashMap<>();
this.unlockedMaterial = new HashSet<>();
this.unlockedBook = new HashSet<>();
this.unlockedTip = new HashSet<>();
this.unlockedView = new HashSet<>();
this.unlockedReliquary = new HashSet<>();
this.unlockedReliquarySuitCodex = new HashSet<>();
}
public PlayerCodex(Player player){
this();
this.player = player;
}
public void setPlayer(Player player) {
this.player = player;
}
public void checkAddedItem(GameItem item){
ItemType type = item.getItemData().getItemType();
if (type == ItemType.ITEM_WEAPON){
if(!getUnlockedWeapon().contains(item.getItemId())){
getUnlockedWeapon().add(item.getItemId());
var codexItem = GameData.getCodexWeaponDataIdMap().get(item.getItemId());
if(codexItem != null){
player.save();
this.player.sendPacket(new PacketCodexDataUpdateNotify(2, codexItem.getId()));
}
}
}
else if(type == ItemType.ITEM_MATERIAL){
if( item.getItemData().getMaterialType() == MaterialType.MATERIAL_FOOD ||
item.getItemData().getMaterialType() == MaterialType.MATERIAL_WIDGET||
item.getItemData().getMaterialType() == MaterialType.MATERIAL_EXCHANGE||
item.getItemData().getMaterialType() == MaterialType.MATERIAL_AVATAR_MATERIAL||
item.getItemData().getMaterialType() == MaterialType.MATERIAL_NOTICE_ADD_HP){
if (!getUnlockedMaterial().contains(item.getItemId())) {
var codexMaterial = GameData.getCodexMaterialDataIdMap().get(item.getItemId());
if (codexMaterial != null) {
getUnlockedMaterial().add(item.getItemId());
player.save();
this.player.sendPacket(new PacketCodexDataUpdateNotify(4, codexMaterial.getId()));
}
}
}
}
else if(type == ItemType.ITEM_RELIQUARY) {
if(!getUnlockedReliquary().contains(item.getItemId())){
getUnlockedReliquary().add(item.getItemId());
checkUnlockedSuits(item);
}
}
}
public void checkAnimal(GameEntity target, CodexAnimalData.CodexAnimalUnlockCondition condition){
if(target instanceof EntityMonster){
var monsterId = ((EntityMonster)target).getMonsterData().getId();
var codexAnimal = GameData.getCodexAnimalDataMap().get(monsterId);
if(!getUnlockedAnimal().containsKey(monsterId)) {
if (codexAnimal != null) {
if(codexAnimal.getUnlockCondition() == condition || codexAnimal.getUnlockCondition() == null){
getUnlockedAnimal().put(monsterId, 1);
}
}
}else{
getUnlockedAnimal().put(monsterId, getUnlockedAnimal().get(monsterId) + 1);
}
player.save();
this.player.sendPacket(new PacketCodexDataUpdateNotify(3, monsterId));
}
}
public void checkUnlockedSuits(GameItem item){
int reliquaryId = item.getItemId();
Optional<CodexReliquaryData> excelReliquarySuitList = GameData.getcodexReliquaryArrayList().stream().filter(
x -> x.getCupId() == reliquaryId
|| x.getLeatherId() == reliquaryId
|| x.getCapId() == reliquaryId
|| x.getFlowerId() == reliquaryId
|| x.getSandId() == reliquaryId
).findFirst();
if(excelReliquarySuitList.isPresent()) {
var excelReliquarySuit = excelReliquarySuitList.get();
if(!getUnlockedReliquarySuitCodex().contains(excelReliquarySuit.getId())){
if(
getUnlockedReliquary().contains(excelReliquarySuit.getCupId()) &&
getUnlockedReliquary().contains(excelReliquarySuit.getLeatherId()) &&
getUnlockedReliquary().contains(excelReliquarySuit.getCapId()) &&
getUnlockedReliquary().contains(excelReliquarySuit.getFlowerId()) &&
getUnlockedReliquary().contains(excelReliquarySuit.getSandId())
){
getUnlockedReliquarySuitCodex().add(excelReliquarySuit.getId());
player.save();
this.player.sendPacket(new PacketCodexDataUpdateNotify(8, excelReliquarySuit.getId()));
}
}
}
}
public Set<Integer> getUnlockedWeapon() {
return unlockedWeapon;
}
public Map<Integer, Integer> getUnlockedAnimal() {
return unlockedAnimal;
}
public Set<Integer> getUnlockedMaterial() {
return unlockedMaterial;
}
public Set<Integer> getUnlockedBook() {
return unlockedBook;
}
public Set<Integer> getUnlockedTip() {
return unlockedTip;
}
public Set<Integer> getUnlockedView() {
return unlockedView;
}
public Set<Integer> getUnlockedReliquary() {
return unlockedReliquary;
}
public Set<Integer> getUnlockedReliquarySuitCodex() {
return unlockedReliquarySuitCodex;
}
}
\ No newline at end of file
...@@ -4,10 +4,10 @@ import java.util.ArrayList; ...@@ -4,10 +4,10 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import dev.morphia.annotations.Entity; import dev.morphia.annotations.Entity;
import emu.grasscutter.GameConstants;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.game.avatar.Avatar; import emu.grasscutter.game.avatar.Avatar;
import static emu.grasscutter.Configuration.*;
@Entity @Entity
public class TeamInfo { public class TeamInfo {
private String name; private String name;
...@@ -15,7 +15,7 @@ public class TeamInfo { ...@@ -15,7 +15,7 @@ public class TeamInfo {
public TeamInfo() { public TeamInfo() {
this.name = ""; this.name = "";
this.avatars = new ArrayList<>(Grasscutter.getConfig().getGameServerOptions().MaxAvatarsInTeam); this.avatars = new ArrayList<>(GAME_OPTIONS.avatarLimits.singlePlayerTeam);
} }
public TeamInfo(List<Integer> avatars) { public TeamInfo(List<Integer> avatars) {
...@@ -44,7 +44,7 @@ public class TeamInfo { ...@@ -44,7 +44,7 @@ public class TeamInfo {
} }
public boolean addAvatar(Avatar avatar) { public boolean addAvatar(Avatar avatar) {
if (size() >= Grasscutter.getConfig().getGameServerOptions().MaxAvatarsInTeam || contains(avatar)) { if (contains(avatar)) {
return false; return false;
} }
...@@ -64,7 +64,7 @@ public class TeamInfo { ...@@ -64,7 +64,7 @@ public class TeamInfo {
} }
public void copyFrom(TeamInfo team) { public void copyFrom(TeamInfo team) {
copyFrom(team, Grasscutter.getConfig().getGameServerOptions().MaxAvatarsInTeam); copyFrom(team, GAME_OPTIONS.avatarLimits.singlePlayerTeam);
} }
public void copyFrom(TeamInfo team, int maxTeamSize) { public void copyFrom(TeamInfo team, int maxTeamSize) {
......
...@@ -5,8 +5,7 @@ import java.util.*; ...@@ -5,8 +5,7 @@ import java.util.*;
import dev.morphia.annotations.Entity; import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Transient; import dev.morphia.annotations.Transient;
import emu.grasscutter.GameConstants; import emu.grasscutter.GameConstants;
import emu.grasscutter.Grasscutter; import emu.grasscutter.data.excels.AvatarSkillDepotData;
import emu.grasscutter.data.def.AvatarSkillDepotData;
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.EntityBaseGadget; import emu.grasscutter.game.entity.EntityBaseGadget;
...@@ -39,6 +38,8 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; ...@@ -39,6 +38,8 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet; import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet; import it.unimi.dsi.fastutil.ints.IntSet;
import static emu.grasscutter.Configuration.*;
@Entity @Entity
public class TeamManager { public class TeamManager {
@Transient private Player player; @Transient private Player player;
...@@ -103,6 +104,20 @@ public class TeamManager { ...@@ -103,6 +104,20 @@ public class TeamManager {
this.mpTeam = mpTeam; this.mpTeam = mpTeam;
} }
/**
* Search through all teams and if the team matches, return that index.
* Otherwise, return -1.
* No match could mean that the team does not currently belong to the player.
*/
public int getTeamId(TeamInfo team) {
for (int i = 1; i <= this.teams.size(); i++) {
if (this.teams.get(i).equals(team)) {
return i;
}
}
return -1;
}
public int getCurrentTeamId() { public int getCurrentTeamId() {
// Starts from 1 // Starts from 1
return currentTeamIndex; return currentTeamIndex;
...@@ -173,17 +188,119 @@ public class TeamManager { ...@@ -173,17 +188,119 @@ public class TeamManager {
public int getMaxTeamSize() { public int getMaxTeamSize() {
if (getPlayer().isInMultiplayer()) { if (getPlayer().isInMultiplayer()) {
int max = Grasscutter.getConfig().getGameServerOptions().MaxAvatarsInTeamMultiplayer; int max = GAME_OPTIONS.avatarLimits.multiplayerTeam;
if (getPlayer().getWorld().getHost() == this.getPlayer()) { if (getPlayer().getWorld().getHost() == this.getPlayer()) {
return Math.max(1, (int) Math.ceil(max / (double) getWorld().getPlayerCount())); return Math.max(1, (int) Math.ceil(max / (double) getWorld().getPlayerCount()));
} }
return Math.max(1, (int) Math.floor(max / (double) getWorld().getPlayerCount())); return Math.max(1, (int) Math.floor(max / (double) getWorld().getPlayerCount()));
} }
return Grasscutter.getConfig().getGameServerOptions().MaxAvatarsInTeam;
return GAME_OPTIONS.avatarLimits.singlePlayerTeam;
} }
// Methods // Methods
/**
* Returns true if there is space to add the number of avatars to the team.
*/
public boolean canAddAvatarsToTeam(TeamInfo team, int avatars) {
return team.size() + avatars <= getMaxTeamSize();
}
/**
* Returns true if there is space to add to the team.
*/
public boolean canAddAvatarToTeam(TeamInfo team) {
return canAddAvatarsToTeam(team, 1);
}
/**
* Returns true if there is space to add the number of avatars to the current team.
* If the current team is temporary, returns false.
*/
public boolean canAddAvatarsToCurrentTeam(int avatars) {
if (this.useTemporarilyTeamIndex != -1){
return false;
}
return canAddAvatarsToTeam(this.getCurrentTeamInfo(), avatars);
}
/**
* Returns true if there is space to add to the current team.
* If the current team is temporary, returns false.
*/
public boolean canAddAvatarToCurrentTeam() {
return canAddAvatarsToCurrentTeam(1);
}
/**
* Try to add the collection of avatars to the team.
* Returns true if all were successfully added.
* If some can not be added, returns false and does not add any.
*/
public boolean addAvatarsToTeam(TeamInfo team, Collection<Avatar> avatars) {
if (!canAddAvatarsToTeam(team, avatars.size())) {
return false;
}
// Convert avatars into a collection of avatar IDs, then add
team.getAvatars().addAll(avatars.stream().map(a -> a.getAvatarId()).toList());
// Update team
if (this.getPlayer().isInMultiplayer()) {
if (team.equals(this.getMpTeam())) {
// MP team Packet
this.updateTeamEntities(new PacketChangeMpTeamAvatarRsp(getPlayer(), team));
}
} else {
// SP team update packet
getPlayer().sendPacket(new PacketAvatarTeamUpdateNotify(getPlayer()));
int teamId = this.getTeamId(team);
if (teamId != -1) {
// This is one of the player's teams
// Update entites
if (teamId == this.getCurrentTeamId()) {
this.updateTeamEntities(new PacketSetUpAvatarTeamRsp(getPlayer(), teamId, team));
} else {
getPlayer().sendPacket(new PacketSetUpAvatarTeamRsp(getPlayer(), teamId, team));
}
}
}
return true;
}
/**
* Try to add an avatar to a team.
* Returns true if successful.
*/
public boolean addAvatarToTeam(TeamInfo team, Avatar avatar){
return addAvatarsToTeam(team, Collections.singleton(avatar));
}
/**
* Try to add the collection of avatars to the current team.
* Will not modify a temporary team.
* Returns true if all were successfully added.
* If some can not be added, returns false and does not add any.
*/
public boolean addAvatarsToCurrentTeam(Collection<Avatar> avatars) {
if (this.useTemporarilyTeamIndex != -1){
return false;
}
return addAvatarsToTeam(this.getCurrentTeamInfo(), avatars);
}
/**
* Try to add an avatar to the current team.
* Will not modify a temporary team.
* Returns true if successful.
*/
public boolean addAvatarToCurrentTeam(Avatar avatar) {
return addAvatarsToCurrentTeam(Collections.singleton(avatar));
}
private void updateTeamResonances() { private void updateTeamResonances() {
Int2IntOpenHashMap map = new Int2IntOpenHashMap(); Int2IntOpenHashMap map = new Int2IntOpenHashMap();
...@@ -235,7 +352,7 @@ public class TeamManager { ...@@ -235,7 +352,7 @@ public class TeamManager {
// Add back entities into team // Add back entities into team
for (int i = 0; i < this.getCurrentTeamInfo().getAvatars().size(); i++) { for (int i = 0; i < this.getCurrentTeamInfo().getAvatars().size(); i++) {
int avatarId = this.getCurrentTeamInfo().getAvatars().get(i); int avatarId = this.getCurrentTeamInfo().getAvatars().get(i);
EntityAvatar entity = null; EntityAvatar entity;
if (existingAvatars.containsKey(avatarId)) { if (existingAvatars.containsKey(avatarId)) {
entity = existingAvatars.get(avatarId); entity = existingAvatars.get(avatarId);
...@@ -302,8 +419,8 @@ public class TeamManager { ...@@ -302,8 +419,8 @@ public class TeamManager {
// Set team data // Set team data
LinkedHashSet<Avatar> newTeam = new LinkedHashSet<>(); LinkedHashSet<Avatar> newTeam = new LinkedHashSet<>();
for (int i = 0; i < list.size(); i++) { for (Long aLong : list) {
Avatar avatar = getPlayer().getAvatars().getAvatarByGuid(list.get(i)); Avatar avatar = getPlayer().getAvatars().getAvatarByGuid(aLong);
if (avatar == null || newTeam.contains(avatar)) { if (avatar == null || newTeam.contains(avatar)) {
// Should never happen // Should never happen
return; return;
...@@ -313,19 +430,7 @@ public class TeamManager { ...@@ -313,19 +430,7 @@ public class TeamManager {
// Clear current team info and add avatars from our new team // Clear current team info and add avatars from our new team
teamInfo.getAvatars().clear(); teamInfo.getAvatars().clear();
for (Avatar avatar : newTeam) { this.addAvatarsToTeam(teamInfo, newTeam);
teamInfo.addAvatar(avatar);
}
// Update packet
getPlayer().sendPacket(new PacketAvatarTeamUpdateNotify(getPlayer()));
// Update entites
if (teamId == this.getCurrentTeamId()) {
this.updateTeamEntities(new PacketSetUpAvatarTeamRsp(getPlayer(), teamId, teamInfo));
} else {
getPlayer().sendPacket(new PacketSetUpAvatarTeamRsp(getPlayer(), teamId, teamInfo));
}
} }
public void setupMpTeam(List<Long> list) { public void setupMpTeam(List<Long> list) {
...@@ -338,8 +443,8 @@ public class TeamManager { ...@@ -338,8 +443,8 @@ public class TeamManager {
// Set team data // Set team data
LinkedHashSet<Avatar> newTeam = new LinkedHashSet<>(); LinkedHashSet<Avatar> newTeam = new LinkedHashSet<>();
for (int i = 0; i < list.size(); i++) { for (Long aLong : list) {
Avatar avatar = getPlayer().getAvatars().getAvatarByGuid(list.get(i)); Avatar avatar = getPlayer().getAvatars().getAvatarByGuid(aLong);
if (avatar == null || newTeam.contains(avatar)) { if (avatar == null || newTeam.contains(avatar)) {
// Should never happen // Should never happen
return; return;
...@@ -349,16 +454,11 @@ public class TeamManager { ...@@ -349,16 +454,11 @@ public class TeamManager {
// Clear current team info and add avatars from our new team // Clear current team info and add avatars from our new team
teamInfo.getAvatars().clear(); teamInfo.getAvatars().clear();
for (Avatar avatar : newTeam) { this.addAvatarsToTeam(teamInfo, newTeam);
teamInfo.addAvatar(avatar);
}
// Packet
this.updateTeamEntities(new PacketChangeMpTeamAvatarRsp(getPlayer(), teamInfo));
} }
public void setupTemporaryTeam(List<List<Long>> guidList) { public void setupTemporaryTeam(List<List<Long>> guidList) {
var team = guidList.stream().map(list -> { this.temporaryTeam = guidList.stream().map(list -> {
// Sanity checks // Sanity checks
if (list.size() == 0 || list.size() > getMaxTeamSize()) { if (list.size() == 0 || list.size() > getMaxTeamSize()) {
return null; return null;
...@@ -366,8 +466,8 @@ public class TeamManager { ...@@ -366,8 +466,8 @@ public class TeamManager {
// Set team data // Set team data
LinkedHashSet<Avatar> newTeam = new LinkedHashSet<>(); LinkedHashSet<Avatar> newTeam = new LinkedHashSet<>();
for (int i = 0; i < list.size(); i++) { for (Long aLong : list) {
Avatar avatar = getPlayer().getAvatars().getAvatarByGuid(list.get(i)); Avatar avatar = getPlayer().getAvatars().getAvatarByGuid(aLong);
if (avatar == null || newTeam.contains(avatar)) { if (avatar == null || newTeam.contains(avatar)) {
// Should never happen // Should never happen
return null; return null;
...@@ -383,7 +483,6 @@ public class TeamManager { ...@@ -383,7 +483,6 @@ public class TeamManager {
.filter(Objects::nonNull) .filter(Objects::nonNull)
.map(TeamInfo::new) .map(TeamInfo::new)
.toList(); .toList();
this.temporaryTeam = team;
} }
public void useTemporaryTeam(int index) { public void useTemporaryTeam(int index) {
...@@ -455,7 +554,7 @@ public class TeamManager { ...@@ -455,7 +554,7 @@ public class TeamManager {
this.setCurrentCharacterIndex(index); this.setCurrentCharacterIndex(index);
// Old entity motion state // Old entity motion state
oldEntity.setMotionState(MotionState.MOTION_STANDBY); oldEntity.setMotionState(MotionState.MOTION_STATE_STANDBY);
// Remove and Add // Remove and Add
getPlayer().getScene().replaceEntity(oldEntity, newEntity); getPlayer().getScene().replaceEntity(oldEntity, newEntity);
...@@ -472,7 +571,7 @@ public class TeamManager { ...@@ -472,7 +571,7 @@ public class TeamManager {
PlayerDieType dieType = deadAvatar.getKilledType(); PlayerDieType dieType = deadAvatar.getKilledType();
int killedBy = deadAvatar.getKilledBy(); int killedBy = deadAvatar.getKilledBy();
if (dieType == PlayerDieType.PLAYER_DIE_DRAWN) { if (dieType == PlayerDieType.PLAYER_DIE_TYPE_DRAWN) {
// Died in water. Do not replace // Died in water. Do not replace
// The official server has skipped this notify and will just respawn the team immediately after the animation. // The official server has skipped this notify and will just respawn the team immediately after the animation.
// TODO: Perhaps find a way to get vanilla experience? // TODO: Perhaps find a way to get vanilla experience?
...@@ -557,7 +656,7 @@ public class TeamManager { ...@@ -557,7 +656,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()) {
...@@ -570,7 +669,7 @@ public class TeamManager { ...@@ -570,7 +669,7 @@ public class TeamManager {
} }
// Teleport player // Teleport player
getPlayer().sendPacket(new PacketPlayerEnterSceneNotify(getPlayer(), EnterType.ENTER_SELF, EnterReason.Revival, 3, GameConstants.START_POSITION)); getPlayer().sendPacket(new PacketPlayerEnterSceneNotify(getPlayer(), EnterType.ENTER_TYPE_SELF, EnterReason.Revival, 3, GameConstants.START_POSITION));
// Set player position // Set player position
player.setSceneId(3); player.setSceneId(3);
......
...@@ -9,21 +9,22 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectMap; ...@@ -9,21 +9,22 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
public enum ElementType { public enum ElementType {
None (0, FightProperty.FIGHT_PROP_MAX_FIRE_ENERGY), None (0, FightProperty.FIGHT_PROP_CUR_FIRE_ENERGY, FightProperty.FIGHT_PROP_MAX_FIRE_ENERGY),
Fire (1, FightProperty.FIGHT_PROP_MAX_FIRE_ENERGY, 10101, "TeamResonance_Fire_Lv2"), Fire (1, FightProperty.FIGHT_PROP_CUR_FIRE_ENERGY, FightProperty.FIGHT_PROP_MAX_FIRE_ENERGY, 10101, "TeamResonance_Fire_Lv2"),
Water (2, FightProperty.FIGHT_PROP_MAX_WATER_ENERGY, 10201, "TeamResonance_Water_Lv2"), Water (2, FightProperty.FIGHT_PROP_CUR_WATER_ENERGY, FightProperty.FIGHT_PROP_MAX_WATER_ENERGY, 10201, "TeamResonance_Water_Lv2"),
Grass (3, FightProperty.FIGHT_PROP_MAX_GRASS_ENERGY), Grass (3, FightProperty.FIGHT_PROP_CUR_GRASS_ENERGY, FightProperty.FIGHT_PROP_MAX_GRASS_ENERGY),
Electric (4, FightProperty.FIGHT_PROP_MAX_ELEC_ENERGY, 10401, "TeamResonance_Electric_Lv2"), Electric (4, FightProperty.FIGHT_PROP_CUR_ELEC_ENERGY, FightProperty.FIGHT_PROP_MAX_ELEC_ENERGY, 10401, "TeamResonance_Electric_Lv2"),
Ice (5, FightProperty.FIGHT_PROP_MAX_ICE_ENERGY, 10601, "TeamResonance_Ice_Lv2"), Ice (5, FightProperty.FIGHT_PROP_CUR_ICE_ENERGY, FightProperty.FIGHT_PROP_MAX_ICE_ENERGY, 10601, "TeamResonance_Ice_Lv2"),
Frozen (6, FightProperty.FIGHT_PROP_MAX_ICE_ENERGY), Frozen (6, FightProperty.FIGHT_PROP_CUR_ICE_ENERGY, FightProperty.FIGHT_PROP_MAX_ICE_ENERGY),
Wind (7, FightProperty.FIGHT_PROP_MAX_WIND_ENERGY, 10301, "TeamResonance_Wind_Lv2"), Wind (7, FightProperty.FIGHT_PROP_CUR_WIND_ENERGY, FightProperty.FIGHT_PROP_MAX_WIND_ENERGY, 10301, "TeamResonance_Wind_Lv2"),
Rock (8, FightProperty.FIGHT_PROP_MAX_ROCK_ENERGY, 10701, "TeamResonance_Rock_Lv2"), Rock (8, FightProperty.FIGHT_PROP_CUR_ROCK_ENERGY, FightProperty.FIGHT_PROP_MAX_ROCK_ENERGY, 10701, "TeamResonance_Rock_Lv2"),
AntiFire (9, FightProperty.FIGHT_PROP_MAX_FIRE_ENERGY), AntiFire (9, FightProperty.FIGHT_PROP_CUR_FIRE_ENERGY, FightProperty.FIGHT_PROP_MAX_FIRE_ENERGY),
Default (255, FightProperty.FIGHT_PROP_MAX_FIRE_ENERGY, 10801, "TeamResonance_AllDifferent"); Default (255, FightProperty.FIGHT_PROP_CUR_FIRE_ENERGY, FightProperty.FIGHT_PROP_MAX_FIRE_ENERGY, 10801, "TeamResonance_AllDifferent");
private final int value; private final int value;
private final int teamResonanceId; private final int teamResonanceId;
private final FightProperty energyProperty; private final FightProperty curEnergyProp;
private final FightProperty maxEnergyProp;
private final int configHash; private final int configHash;
private static final Int2ObjectMap<ElementType> map = new Int2ObjectOpenHashMap<>(); private static final Int2ObjectMap<ElementType> map = new Int2ObjectOpenHashMap<>();
private static final Map<String, ElementType> stringMap = new HashMap<>(); private static final Map<String, ElementType> stringMap = new HashMap<>();
...@@ -35,13 +36,14 @@ public enum ElementType { ...@@ -35,13 +36,14 @@ public enum ElementType {
}); });
} }
private ElementType(int value, FightProperty energyProperty) { private ElementType(int value, FightProperty curEnergyProp, FightProperty maxEnergyProp) {
this(value, energyProperty, 0, null); this(value, curEnergyProp, maxEnergyProp, 0, null);
} }
private ElementType(int value, FightProperty energyProperty, int teamResonanceId, String configName) { private ElementType(int value, FightProperty curEnergyProp, FightProperty maxEnergyProp, int teamResonanceId, String configName) {
this.value = value; this.value = value;
this.energyProperty = energyProperty; this.curEnergyProp = curEnergyProp;
this.maxEnergyProp = maxEnergyProp;
this.teamResonanceId = teamResonanceId; this.teamResonanceId = teamResonanceId;
if (configName != null) { if (configName != null) {
this.configHash = Utils.abilityHash(configName); this.configHash = Utils.abilityHash(configName);
...@@ -54,8 +56,12 @@ public enum ElementType { ...@@ -54,8 +56,12 @@ public enum ElementType {
return value; return value;
} }
public FightProperty getEnergyProperty() { public FightProperty getCurEnergyProp() {
return energyProperty; return curEnergyProp;
}
public FightProperty getMaxEnergyProp() {
return maxEnergyProp;
} }
public int getTeamResonanceId() { public int getTeamResonanceId() {
......
package emu.grasscutter.game.props;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Stream;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
public enum MonsterType {
MONSTER_NONE (0),
MONSTER_ORDINARY (1),
MONSTER_BOSS (2),
MONSTER_ENV_ANIMAL (3),
MONSTER_LITTLE_MONSTER (4),
MONSTER_FISH (5);
private final int value;
private static final Int2ObjectMap<MonsterType> map = new Int2ObjectOpenHashMap<>();
private static final Map<String, MonsterType> stringMap = new HashMap<>();
static {
Stream.of(values()).forEach(e -> {
map.put(e.getValue(), e);
stringMap.put(e.name(), e);
});
}
private MonsterType(int value) {
this.value = value;
}
public int getValue() {
return value;
}
public static MonsterType getTypeByValue(int value) {
return map.getOrDefault(value, MONSTER_NONE);
}
public static MonsterType getTypeByName(String name) {
return stringMap.getOrDefault(name, MONSTER_NONE);
}
}
...@@ -94,6 +94,7 @@ public enum OpenState { ...@@ -94,6 +94,7 @@ public enum OpenState {
OPEN_STATE_GUIDE_RELICRESOLVE(84), OPEN_STATE_GUIDE_RELICRESOLVE(84),
OPEN_STATE_GUIDE_GGUIDE(85), OPEN_STATE_GUIDE_GGUIDE(85),
OPEN_STATE_GUIDE_GGUIDE_HINT(86), OPEN_STATE_GUIDE_GGUIDE_HINT(86),
OPEN_STATE_GUIDE_RIGHT_TEAM(90), // mobile phone only!
OPEN_STATE_CITY_REPUATION_MENGDE(800), OPEN_STATE_CITY_REPUATION_MENGDE(800),
OPEN_STATE_CITY_REPUATION_LIYUE(801), OPEN_STATE_CITY_REPUATION_LIYUE(801),
OPEN_STATE_CITY_REPUATION_UI_HINT(802), OPEN_STATE_CITY_REPUATION_UI_HINT(802),
......
...@@ -30,7 +30,7 @@ public enum PlayerProperty { ...@@ -30,7 +30,7 @@ public enum PlayerProperty {
// his gems and then got a money refund, so negative is allowed. // his gems and then got a money refund, so negative is allowed.
PROP_PLAYER_SCOIN (10016), // Mora [0, +inf) PROP_PLAYER_SCOIN (10016), // Mora [0, +inf)
PROP_PLAYER_MP_SETTING_TYPE (10017), // Do you allow other players to join your game? [0=no 1=direct 2=approval] PROP_PLAYER_MP_SETTING_TYPE (10017), // Do you allow other players to join your game? [0=no 1=direct 2=approval]
PROP_IS_MP_MODE_AVAILABLE (10018), // Are you not in a quest or something that disables MP? [0, 1] PROP_IS_MP_MODE_AVAILABLE (10018), // 0 if in quest or something that disables MP [0, 1]
PROP_PLAYER_WORLD_LEVEL (10019), // [0, 8] PROP_PLAYER_WORLD_LEVEL (10019), // [0, 8]
PROP_PLAYER_RESIN (10020), // Original Resin [0, +inf) PROP_PLAYER_RESIN (10020), // Original Resin [0, +inf)
PROP_PLAYER_WAIT_SUB_HCOIN (10022), PROP_PLAYER_WAIT_SUB_HCOIN (10022),
......
package emu.grasscutter.game.props;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Stream;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
public enum WeaponType {
WEAPON_NONE (0),
WEAPON_SWORD_ONE_HAND (1, 10, 5),
WEAPON_CROSSBOW (2),
WEAPON_STAFF (3),
WEAPON_DOUBLE_DAGGER (4),
WEAPON_KATANA (5),
WEAPON_SHURIKEN (6),
WEAPON_STICK (7),
WEAPON_SPEAR (8),
WEAPON_SHIELD_SMALL (9),
WEAPON_CATALYST (10, 0, 10),
WEAPON_CLAYMORE (11, 0, 10),
WEAPON_BOW (12, 0, 5),
WEAPON_POLE (13, 0, 4);
private final int value;
private int energyGainInitialProbability;
private int energyGainIncreaseProbability;
private static final Int2ObjectMap<WeaponType> map = new Int2ObjectOpenHashMap<>();
private static final Map<String, WeaponType> stringMap = new HashMap<>();
static {
Stream.of(values()).forEach(e -> {
map.put(e.getValue(), e);
stringMap.put(e.name(), e);
});
}
private WeaponType(int value) {
this.value = value;
}
private WeaponType(int value, int energyGainInitialProbability, int energyGainIncreaseProbability) {
this.value = value;
this.energyGainInitialProbability = energyGainInitialProbability;
this.energyGainIncreaseProbability = energyGainIncreaseProbability;
}
public int getValue() {
return value;
}
public int getEnergyGainInitialProbability() {
return energyGainInitialProbability;
}
public int getEnergyGainIncreaseProbability() {
return energyGainIncreaseProbability;
}
public static WeaponType getTypeByValue(int value) {
return map.getOrDefault(value, WEAPON_NONE);
}
public static WeaponType getTypeByName(String name) {
return stringMap.getOrDefault(name, WEAPON_NONE);
}
}
package emu.grasscutter.game.quest;
import java.util.HashMap;
import java.util.Map;
import emu.grasscutter.server.packet.send.PacketCodexDataUpdateNotify;
import org.bson.types.ObjectId;
import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Id;
import dev.morphia.annotations.Indexed;
import dev.morphia.annotations.Transient;
import emu.grasscutter.data.GameData;
import emu.grasscutter.data.binout.MainQuestData;
import emu.grasscutter.data.excels.RewardData;
import emu.grasscutter.database.DatabaseHelper;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.props.ActionReason;
import emu.grasscutter.game.quest.enums.ParentQuestState;
import emu.grasscutter.game.quest.enums.QuestState;
import emu.grasscutter.net.proto.ChildQuestOuterClass.ChildQuest;
import emu.grasscutter.net.proto.ParentQuestOuterClass.ParentQuest;
import emu.grasscutter.net.proto.QuestOuterClass.Quest;
import emu.grasscutter.server.packet.send.PacketFinishedParentQuestUpdateNotify;
import emu.grasscutter.server.packet.send.PacketQuestListUpdateNotify;
import emu.grasscutter.server.packet.send.PacketQuestProgressUpdateNotify;
import emu.grasscutter.utils.Utils;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
@Entity(value = "quests", useDiscriminator = false)
public class GameMainQuest {
@Id private ObjectId id;
@Indexed private int ownerUid;
@Transient private Player owner;
private Map<Integer, GameQuest> childQuests;
private int parentQuestId;
private int[] questVars;
private ParentQuestState state;
private boolean isFinished;
@Deprecated // Morphia only. Do not use.
public GameMainQuest() {}
public GameMainQuest(Player player, int parentQuestId) {
this.owner = player;
this.ownerUid = player.getUid();
this.parentQuestId = parentQuestId;
this.childQuests = new HashMap<>();
this.questVars = new int[5];
this.state = ParentQuestState.PARENT_QUEST_STATE_NONE;
}
public int getParentQuestId() {
return parentQuestId;
}
public int getOwnerUid() {
return ownerUid;
}
public Player getOwner() {
return owner;
}
public void setOwner(Player player) {
if (player.getUid() != this.getOwnerUid()) return;
this.owner = player;
}
public Map<Integer, GameQuest> getChildQuests() {
return childQuests;
}
public GameQuest getChildQuestById(int id) {
return this.getChildQuests().get(id);
}
public int[] getQuestVars() {
return questVars;
}
public ParentQuestState getState() {
return state;
}
public boolean isFinished() {
return isFinished;
}
public void finish() {
this.isFinished = true;
this.state = ParentQuestState.PARENT_QUEST_STATE_FINISHED;
this.getOwner().getSession().send(new PacketFinishedParentQuestUpdateNotify(this));
this.getOwner().getSession().send(new PacketCodexDataUpdateNotify(this));
this.save();
// Add rewards
MainQuestData mainQuestData = GameData.getMainQuestDataMap().get(this.getParentQuestId());
for (int rewardId : mainQuestData.getRewardIdList()) {
RewardData rewardData = GameData.getRewardDataMap().get(rewardId);
if (rewardData == null) {
continue;
}
getOwner().getInventory().addItemParamDatas(rewardData.getRewardItemList(), ActionReason.QuestReward);
}
}
public void save() {
DatabaseHelper.saveQuest(this);
}
public ParentQuest toProto() {
ParentQuest.Builder proto = ParentQuest.newBuilder()
.setParentQuestId(getParentQuestId())
.setIsFinished(isFinished())
.setParentQuestState(getState().getValue());
for (GameQuest quest : this.getChildQuests().values()) {
ChildQuest childQuest = ChildQuest.newBuilder()
.setQuestId(quest.getQuestId())
.setState(quest.getState().getValue())
.build();
proto.addChildQuestList(childQuest);
}
if (getQuestVars() != null) {
for (int i : getQuestVars()) {
proto.addQuestVar(i);
}
}
return proto.build();
}
}
package emu.grasscutter.game.quest;
import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Transient;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.GameData;
import emu.grasscutter.data.binout.MainQuestData;
import emu.grasscutter.data.binout.MainQuestData.SubQuestData;
import emu.grasscutter.data.excels.QuestData;
import emu.grasscutter.data.excels.QuestData.QuestCondition;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.quest.enums.LogicType;
import emu.grasscutter.game.quest.enums.QuestState;
import emu.grasscutter.net.proto.QuestOuterClass.Quest;
import emu.grasscutter.server.packet.send.PacketCodexDataUpdateNotify;
import emu.grasscutter.server.packet.send.PacketQuestListUpdateNotify;
import emu.grasscutter.server.packet.send.PacketQuestProgressUpdateNotify;
import emu.grasscutter.utils.Utils;
@Entity
public class GameQuest {
@Transient private GameMainQuest mainQuest;
@Transient private QuestData questData;
private int questId;
private int mainQuestId;
private QuestState state;
private int startTime;
private int acceptTime;
private int finishTime;
private int[] finishProgressList;
private int[] failProgressList;
@Deprecated // Morphia only. Do not use.
public GameQuest() {}
public GameQuest(GameMainQuest mainQuest, QuestData questData) {
this.mainQuest = mainQuest;
this.questId = questData.getId();
this.mainQuestId = questData.getMainId();
this.questData = questData;
this.acceptTime = Utils.getCurrentSeconds();
this.startTime = this.acceptTime;
this.state = QuestState.QUEST_STATE_UNFINISHED;
if (questData.getFinishCond() != null && questData.getAcceptCond().length != 0) {
this.finishProgressList = new int[questData.getFinishCond().length];
}
if (questData.getFailCond() != null && questData.getFailCond().length != 0) {
this.failProgressList = new int[questData.getFailCond().length];
}
this.mainQuest.getChildQuests().put(this.questId, this);
}
public GameMainQuest getMainQuest() {
return mainQuest;
}
public void setMainQuest(GameMainQuest mainQuest) {
this.mainQuest = mainQuest;
}
public Player getOwner() {
return getMainQuest().getOwner();
}
public int getQuestId() {
return questId;
}
public int getMainQuestId() {
return mainQuestId;
}
public QuestData getData() {
return questData;
}
public void setConfig(QuestData config) {
if (this.getQuestId() != config.getId()) return;
this.questData = config;
}
public QuestState getState() {
return state;
}
public void setState(QuestState state) {
this.state = state;
}
public int getStartTime() {
return startTime;
}
public void setStartTime(int startTime) {
this.startTime = startTime;
}
public int getAcceptTime() {
return acceptTime;
}
public void setAcceptTime(int acceptTime) {
this.acceptTime = acceptTime;
}
public int getFinishTime() {
return finishTime;
}
public void setFinishTime(int finishTime) {
this.finishTime = finishTime;
}
public int[] getFinishProgressList() {
return finishProgressList;
}
public void setFinishProgress(int index, int value) {
finishProgressList[index] = value;
}
public int[] getFailProgressList() {
return failProgressList;
}
public void setFailProgress(int index, int value) {
failProgressList[index] = value;
}
public void finish() {
this.state = QuestState.QUEST_STATE_FINISHED;
this.finishTime = Utils.getCurrentSeconds();
if (this.getFinishProgressList() != null) {
for (int i = 0 ; i < getFinishProgressList().length; i++) {
getFinishProgressList()[i] = 1;
}
}
this.getOwner().getSession().send(new PacketQuestProgressUpdateNotify(this));
this.getOwner().getSession().send(new PacketQuestListUpdateNotify(this));
if (this.getData().finishParent()) {
// This quest finishes the questline - the main quest will also save the quest to db so we dont have to call save() here
this.getMainQuest().finish();
} else {
// Try and accept other quests if possible
this.tryAcceptQuestLine();
this.save();
}
}
public boolean tryAcceptQuestLine() {
try {
MainQuestData questConfig = GameData.getMainQuestDataMap().get(this.getMainQuestId());
for (SubQuestData subQuest : questConfig.getSubQuests()) {
GameQuest quest = getMainQuest().getChildQuestById(subQuest.getSubId());
if (quest == null) {
QuestData questData = GameData.getQuestDataMap().get(subQuest.getSubId());
if (questData == null || questData.getAcceptCond() == null
|| questData.getAcceptCond().length == 0) {
continue;
}
int[] accept = new int[questData.getAcceptCond().length];
// TODO
for (int i = 0; i < questData.getAcceptCond().length; i++) {
QuestCondition condition = questData.getAcceptCond()[i];
boolean result = getOwner().getServer().getQuestHandler().triggerCondition(this, condition,
condition.getParam());
accept[i] = result ? 1 : 0;
}
boolean shouldAccept = LogicType.calculate(questData.getAcceptCondComb(), accept);
if (shouldAccept) {
this.getOwner().getQuestManager().addQuest(questData.getId());
}
}
}
} catch (Exception e) {
Grasscutter.getLogger().error("An error occurred while trying to accept quest.", e);
}
return false;
}
public void save() {
getMainQuest().save();
}
public Quest toProto() {
Quest.Builder proto = Quest.newBuilder()
.setQuestId(this.getQuestId())
.setState(this.getState().getValue())
.setParentQuestId(this.getMainQuestId())
.setStartTime(this.getStartTime())
.setStartGameTime(438)
.setAcceptTime(this.getAcceptTime());
if (this.getFinishProgressList() != null) {
for (int i : this.getFinishProgressList()) {
proto.addFinishProgressList(i);
}
}
if (this.getFailProgressList() != null) {
for (int i : this.getFailProgressList()) {
proto.addFailProgressList(i);
}
}
return proto.build();
}
}
package emu.grasscutter.game.quest;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import emu.grasscutter.data.GameData;
import emu.grasscutter.data.excels.QuestData;
import emu.grasscutter.data.excels.QuestData.QuestCondition;
import emu.grasscutter.database.DatabaseHelper;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.quest.enums.QuestTrigger;
import emu.grasscutter.game.quest.enums.LogicType;
import emu.grasscutter.game.quest.enums.QuestState;
import emu.grasscutter.server.packet.send.PacketFinishedParentQuestUpdateNotify;
import emu.grasscutter.server.packet.send.PacketQuestListUpdateNotify;
import emu.grasscutter.server.packet.send.PacketQuestProgressUpdateNotify;
import emu.grasscutter.server.packet.send.PacketServerCondMeetQuestListUpdateNotify;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
public class QuestManager {
private final Player player;
private final Int2ObjectMap<GameMainQuest> quests;
public QuestManager(Player player) {
this.player = player;
this.quests = new Int2ObjectOpenHashMap<>();
}
public Player getPlayer() {
return player;
}
public Int2ObjectMap<GameMainQuest> getQuests() {
return quests;
}
public GameMainQuest getMainQuestById(int mainQuestId) {
return getQuests().get(mainQuestId);
}
public GameQuest getQuestById(int questId) {
QuestData questConfig = GameData.getQuestDataMap().get(questId);
if (questConfig == null) {
return null;
}
GameMainQuest mainQuest = getQuests().get(questConfig.getMainId());
if (mainQuest == null) {
return null;
}
return mainQuest.getChildQuests().get(questId);
}
public void forEachQuest(Consumer<GameQuest> callback) {
for (GameMainQuest mainQuest : getQuests().values()) {
for (GameQuest quest : mainQuest.getChildQuests().values()) {
callback.accept(quest);
}
}
}
public void forEachMainQuest(Consumer<GameMainQuest> callback) {
for (GameMainQuest mainQuest : getQuests().values()) {
callback.accept(mainQuest);
}
}
// TODO
public void forEachActiveQuest(Consumer<GameQuest> callback) {
for (GameMainQuest mainQuest : getQuests().values()) {
for (GameQuest quest : mainQuest.getChildQuests().values()) {
if (quest.getState() != QuestState.QUEST_STATE_FINISHED) {
callback.accept(quest);
}
}
}
}
public GameMainQuest addMainQuest(QuestData questConfig) {
GameMainQuest mainQuest = new GameMainQuest(getPlayer(), questConfig.getMainId());
getQuests().put(mainQuest.getParentQuestId(), mainQuest);
getPlayer().sendPacket(new PacketFinishedParentQuestUpdateNotify(mainQuest));
return mainQuest;
}
public GameQuest addQuest(int questId) {
QuestData questConfig = GameData.getQuestDataMap().get(questId);
if (questConfig == null) {
return null;
}
// Main quest
GameMainQuest mainQuest = this.getMainQuestById(questConfig.getMainId());
// Create main quest if it doesnt exist
if (mainQuest == null) {
mainQuest = addMainQuest(questConfig);
}
// Sub quest
GameQuest quest = mainQuest.getChildQuestById(questId);
if (quest != null) {
return null;
}
// Create
quest = new GameQuest(mainQuest, questConfig);
// Save main quest
mainQuest.save();
// Send packet
getPlayer().sendPacket(new PacketQuestListUpdateNotify(quest));
return quest;
}
public void triggerEvent(QuestTrigger condType, int... params) {
Set<GameQuest> changedQuests = new HashSet<>();
this.forEachActiveQuest(quest -> {
QuestData data = quest.getData();
for (int i = 0; i < data.getFinishCond().length; i++) {
if (quest.getFinishProgressList() == null
|| quest.getFinishProgressList().length == 0
|| quest.getFinishProgressList()[i] == 1) {
continue;
}
QuestCondition condition = data.getFinishCond()[i];
if (condition.getType() != condType) {
continue;
}
boolean result = getPlayer().getServer().getQuestHandler().triggerContent(quest, condition, params);
if (result) {
quest.getFinishProgressList()[i] = 1;
changedQuests.add(quest);
}
}
});
for (GameQuest quest : changedQuests) {
LogicType logicType = quest.getData().getFailCondComb();
int[] progress = quest.getFinishProgressList();
// Handle logical comb
boolean finish = LogicType.calculate(logicType, progress);
// Finish
if (finish) {
quest.finish();
} else {
getPlayer().sendPacket(new PacketQuestProgressUpdateNotify(quest));
quest.save();
}
}
}
public void loadFromDatabase() {
List<GameMainQuest> quests = DatabaseHelper.getAllQuests(getPlayer());
for (GameMainQuest mainQuest : quests) {
mainQuest.setOwner(this.getPlayer());
for (GameQuest quest : mainQuest.getChildQuests().values()) {
quest.setMainQuest(mainQuest);
quest.setConfig(GameData.getQuestDataMap().get(quest.getQuestId()));
}
this.getQuests().put(mainQuest.getParentQuestId(), mainQuest);
}
}
}
package emu.grasscutter.game.quest;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import emu.grasscutter.game.quest.enums.QuestTrigger;
@Retention(RetentionPolicy.RUNTIME)
public @interface QuestValue {
QuestTrigger value();
}
package emu.grasscutter.game.quest;
import java.util.Set;
import org.reflections.Reflections;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.excels.QuestData.QuestCondition;
import emu.grasscutter.game.quest.handlers.QuestBaseHandler;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
@SuppressWarnings("unchecked")
public class ServerQuestHandler {
private final Int2ObjectMap<QuestBaseHandler> condHandlers;
private final Int2ObjectMap<QuestBaseHandler> contHandlers;
private final Int2ObjectMap<QuestBaseHandler> execHandlers;
public ServerQuestHandler() {
this.condHandlers = new Int2ObjectOpenHashMap<>();
this.contHandlers = new Int2ObjectOpenHashMap<>();
this.execHandlers = new Int2ObjectOpenHashMap<>();
this.registerHandlers();
}
public void registerHandlers() {
this.registerHandlers(this.condHandlers, "emu.grasscutter.game.quest.conditions");
this.registerHandlers(this.contHandlers, "emu.grasscutter.game.quest.content");
this.registerHandlers(this.execHandlers, "emu.grasscutter.game.quest.exec");
}
public void registerHandlers(Int2ObjectMap<QuestBaseHandler> map, String packageName) {
Reflections reflections = new Reflections(packageName);
Set<?> handlerClasses = reflections.getSubTypesOf(QuestBaseHandler.class);
for (Object obj : handlerClasses) {
this.registerPacketHandler(map, (Class<? extends QuestBaseHandler>) obj);
}
}
public void registerPacketHandler(Int2ObjectMap<QuestBaseHandler> map, Class<? extends QuestBaseHandler> handlerClass) {
try {
QuestValue opcode = handlerClass.getAnnotation(QuestValue.class);
if (opcode == null || opcode.value().getValue() <= 0) {
return;
}
QuestBaseHandler packetHandler = (QuestBaseHandler) handlerClass.newInstance();
map.put(opcode.value().getValue(), packetHandler);
} catch (Exception e) {
e.printStackTrace();
}
}
// TODO make cleaner
public boolean triggerCondition(GameQuest quest, QuestCondition condition, int... params) {
QuestBaseHandler handler = condHandlers.get(condition.getType().getValue());
if (handler == null || quest.getData() == null) {
return false;
}
return handler.execute(quest, condition, params);
}
public boolean triggerContent(GameQuest quest, QuestCondition condition, int... params) {
QuestBaseHandler handler = contHandlers.get(condition.getType().getValue());
if (handler == null || quest.getData() == null) {
return false;
}
return handler.execute(quest, condition, params);
}
public boolean triggerExec(GameQuest quest, QuestCondition condition, int... params) {
QuestBaseHandler handler = execHandlers.get(condition.getType().getValue());
if (handler == null || quest.getData() == null) {
return false;
}
return handler.execute(quest, condition, params);
}
}
package emu.grasscutter.game.quest.conditions;
import emu.grasscutter.game.quest.QuestValue;
import emu.grasscutter.data.excels.QuestData.QuestCondition;
import emu.grasscutter.game.quest.GameQuest;
import emu.grasscutter.game.quest.enums.QuestTrigger;
import emu.grasscutter.game.quest.handlers.QuestBaseHandler;
@QuestValue(QuestTrigger.QUEST_CONTENT_NONE)
public class BaseCondition extends QuestBaseHandler {
@Override
public boolean execute(GameQuest quest, QuestCondition condition, int... params) {
// TODO Auto-generated method stub
return false;
}
}
package emu.grasscutter.game.quest.conditions;
import emu.grasscutter.game.quest.QuestValue;
import emu.grasscutter.data.excels.QuestData.QuestCondition;
import emu.grasscutter.game.quest.GameQuest;
import emu.grasscutter.game.quest.enums.QuestTrigger;
import emu.grasscutter.game.quest.handlers.QuestBaseHandler;
@QuestValue(QuestTrigger.QUEST_COND_PLAYER_LEVEL_EQUAL_GREATER)
public class ConditionPlayerLevelEqualGreater extends QuestBaseHandler {
@Override
public boolean execute(GameQuest quest, QuestCondition condition, int... params) {
return quest.getOwner().getLevel() >= params[0];
}
}
package emu.grasscutter.game.quest.conditions;
import emu.grasscutter.game.quest.QuestValue;
import emu.grasscutter.data.excels.QuestData.QuestCondition;
import emu.grasscutter.game.quest.GameQuest;
import emu.grasscutter.game.quest.enums.QuestTrigger;
import emu.grasscutter.game.quest.handlers.QuestBaseHandler;
@QuestValue(QuestTrigger.QUEST_COND_STATE_EQUAL)
public class ConditionStateEqual extends QuestBaseHandler {
@Override
public boolean execute(GameQuest quest, QuestCondition condition, int... params) {
GameQuest checkQuest = quest.getOwner().getQuestManager().getQuestById(params[0]);
if (checkQuest != null) {
return checkQuest.getState().getValue() == params[1];
}
return false;
}
}
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