Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix revives; improve dungeon exit flow #2409

Merged
merged 1 commit into from
Oct 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import emu.grasscutter.data.binout.AbilityModifier.AbilityModifierAction;
import emu.grasscutter.game.ability.actions.*;
import emu.grasscutter.game.ability.mixins.*;
import emu.grasscutter.game.entity.EntityAvatar;
import emu.grasscutter.game.entity.GameEntity;
import emu.grasscutter.game.player.*;
import emu.grasscutter.game.props.FightProperty;
Expand Down Expand Up @@ -562,6 +563,14 @@ private void handleKillState(AbilityInvokeEntry invoke) throws InvalidProtocolBu
if (killState.getKilled()) {
scene.killEntity(entity);
} else if (!entity.isAlive()) {
if (entity instanceof EntityAvatar) {
// TODO Should EntityAvatar act on this invocation?
// It bugs revival due to resetting HP to max when
// the avatar should just stay dead.
Grasscutter.getLogger()
.trace("Entity of ID {} is EntityAvatar. Ignoring", invoke.getEntityId());
return;
}
entity.setFightProperty(
FightProperty.FIGHT_PROP_CUR_HP,
entity.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP));
Expand Down
16 changes: 12 additions & 4 deletions src/main/java/emu/grasscutter/game/dungeons/DungeonSystem.java
Original file line number Diff line number Diff line change
Expand Up @@ -168,13 +168,21 @@ public void exitDungeon(Player player) {
dungeonManager.unsetTrialTeam(player);
}
// clean temp team if it has
player.getTeamManager().cleanTemporaryTeam();
if (!player.getTeamManager().cleanTemporaryTeam())
{
// no temp team. Will use real current team, but check
// for any dead avatar to prevent switching into them.
player.getTeamManager().checkCurrentAvatarIsAlive(null);
}
player.getTowerManager().clearEntry();
dungeonManager.setTowerDungeon(false);

// Transfer player back to world
player.getWorld().transferPlayerToScene(player, prevScene, prevPos);
player.sendPacket(new BasePacket(PacketOpcodes.PlayerQuitDungeonRsp));
// Transfer player back to world after a small delay.
// This wait is important for avoiding double teleports,
// which specifically happen when player quits a dungeon
// by teleporting to map waypoints.
// From testing, 200ms seem reasonable.
player.getWorld().queueTransferPlayerToScene(player, prevScene, prevPos, 200);
}

public void restartDungeon(Player player) {
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/emu/grasscutter/game/entity/EntityAvatar.java
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ public EntityAvatar(Scene scene, Avatar avatar) {
}

this.initAbilities();

// New EntityAvatar instances are created on every scene transition.
// Ensure that isDead is properly carried over between scenes.
// Otherwise avatars could have 0 HP but not considered dead.
this.checkIfDead();
}

@Override
Expand Down
21 changes: 14 additions & 7 deletions src/main/java/emu/grasscutter/game/entity/GameEntity.java
Original file line number Diff line number Diff line change
Expand Up @@ -174,13 +174,7 @@ public void damage(float amount, int killerId, ElementType attackType) {
}

this.lastAttackType = attackType;

// Check if dead
if (this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP) <= 0f) {
this.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, 0f);
this.isDead = true;
}

this.checkIfDead();
this.runLuaCallbacks(event);

// Packets
Expand All @@ -194,6 +188,17 @@ public void damage(float amount, int killerId, ElementType attackType) {
}
}

public void checkIfDead() {
if (this.getFightProperties() == null || !hasFightProperty(FightProperty.FIGHT_PROP_CUR_HP)) {
return;
}

if (this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP) <= 0f) {
this.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, 0f);
this.isDead = true;
}
}

/**
* Runs the Lua callbacks for {@link EntityDamageEvent}.
*
Expand Down Expand Up @@ -333,6 +338,8 @@ public void onDeath(int killerId) {
if (entityController != null) {
entityController.onDie(this, getLastAttackType());
}

this.isDead = true;
}

/** Invoked when a global ability value is updated. */
Expand Down
1 change: 1 addition & 0 deletions src/main/java/emu/grasscutter/game/player/Player.java
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ public class Player implements PlayerHook, FieldFetch {
@Getter @Setter private Set<Date> moonCardGetTimes;

@Transient @Getter private boolean paused;
@Transient @Getter @Setter private Future<?> queuedTeleport;
@Transient @Getter @Setter private int enterSceneToken;
@Transient @Getter @Setter private SceneLoadState sceneLoadState = SceneLoadState.NONE;
@Transient private boolean hasSentLoginPackets;
Expand Down
68 changes: 48 additions & 20 deletions src/main/java/emu/grasscutter/game/player/TeamManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,30 @@ public void updateTeamEntities(BasePacket responsePacket) {
this.getPlayer().sendPacket(responsePacket);
}

// Ensure new selected character index is alive.
// If not, change to another alive one or revive.
checkCurrentAvatarIsAlive(currentEntity);
}

public void checkCurrentAvatarIsAlive(EntityAvatar currentEntity) {
if (currentEntity == null) {
currentEntity = this.getCurrentAvatarEntity();
}

// Ensure currently selected character is still alive
if (!this.getActiveTeam().get(this.currentCharacterIndex).isAlive()) {
// Character died in a dungeon challenge...
int replaceIndex = getDeadAvatarReplacement();
if (0 <= replaceIndex && replaceIndex < this.getActiveTeam().size()) {
this.currentCharacterIndex = replaceIndex;
} else {
// Team wiped in dungeon...
// Revive and change to first avatar.
this.currentCharacterIndex = 0;
this.reviveAvatar(this.getCurrentAvatarEntity().getAvatar());
}
}

// Check if character changed
var newAvatarEntity = this.getCurrentAvatarEntity();
if (currentEntity != null && newAvatarEntity != null && currentEntity != newAvatarEntity) {
Expand Down Expand Up @@ -700,15 +724,16 @@ public void useTemporaryTeam(int index) {
this.updateTeamEntities(null);
}

public void cleanTemporaryTeam() {
public boolean cleanTemporaryTeam() {
// check if using temporary team
if (useTemporarilyTeamIndex < 0) {
return;
return false;
}

this.useTemporarilyTeamIndex = -1;
this.temporaryTeam = null;
this.updateTeamEntities(null);
return true;
}

public synchronized void setCurrentTeam(int teamId) {
Expand Down Expand Up @@ -810,38 +835,41 @@ public void onAvatarDie(long dieGuid) {
// TODO: Perhaps find a way to get vanilla experience?
this.getPlayer().sendPacket(new PacketWorldPlayerDieNotify(dieType, killedBy));
} else {
// Replacement avatar
EntityAvatar replacement = null;
int replaceIndex = -1;

for (int i = 0; i < this.getActiveTeam().size(); i++) {
EntityAvatar entity = this.getActiveTeam().get(i);
if (entity.isAlive()) {
replaceIndex = i;
replacement = entity;
break;
}
}

if (replacement == null) {
// Find replacement avatar
int replaceIndex = getDeadAvatarReplacement();
if (0 <= replaceIndex && replaceIndex < this.getActiveTeam().size()) {
// Set index and spawn replacement member
this.setCurrentCharacterIndex(replaceIndex);
this.getPlayer().getScene().addEntity(this.getActiveTeam().get(replaceIndex));
} else {
// No more living team members...
this.getPlayer().sendPacket(new PacketWorldPlayerDieNotify(dieType, killedBy));
// Invoke player team death event.
PlayerTeamDeathEvent event =
new PlayerTeamDeathEvent(
this.getPlayer(), this.getActiveTeam().get(this.getCurrentCharacterIndex()));
event.call();
} else {
// Set index and spawn replacement member
this.setCurrentCharacterIndex(replaceIndex);
this.getPlayer().getScene().addEntity(replacement);
}
}

// Response packet
this.getPlayer().sendPacket(new PacketAvatarDieAnimationEndRsp(deadAvatar.getId(), 0));
}

public int getDeadAvatarReplacement() {
int replaceIndex = -1;

for (int i = 0; i < this.getActiveTeam().size(); i++) {
EntityAvatar entity = this.getActiveTeam().get(i);
if (entity.isAlive()) {
replaceIndex = i;
break;
}
}

return replaceIndex;
}

public boolean reviveAvatar(Avatar avatar) {
for (EntityAvatar entity : this.getActiveTeam()) {
if (entity.getAvatar() == avatar) {
Expand Down
34 changes: 34 additions & 0 deletions src/main/java/emu/grasscutter/game/world/World.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import static emu.grasscutter.server.event.player.PlayerTeleportEvent.TeleportType.SCRIPT;

import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.GameData;
import emu.grasscutter.data.excels.dungeon.DungeonData;
import emu.grasscutter.game.entity.*;
Expand All @@ -19,8 +20,10 @@
import emu.grasscutter.server.game.GameServer;
import emu.grasscutter.server.packet.send.*;
import emu.grasscutter.utils.ConversionUtils;
import io.netty.util.concurrent.FastThreadLocalThread;
import it.unimi.dsi.fastutil.ints.*;
import java.util.*;
import java.util.concurrent.*;
import lombok.*;
import org.jetbrains.annotations.NotNull;

Expand All @@ -43,6 +46,16 @@ public class World implements Iterable<Player> {
@Getter private boolean isPaused = false;
@Getter private long currentWorldTime;

private static final ExecutorService eventExecutor =
new ThreadPoolExecutor(
4,
4,
60,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(1000),
FastThreadLocalThread::new,
new ThreadPoolExecutor.AbortPolicy());

public World(Player player) {
this(player, false);
}
Expand Down Expand Up @@ -311,6 +324,17 @@ public void save() {
this.getScenes().values().forEach(Scene::saveGroups);
}

public void queueTransferPlayerToScene(Player player, int sceneId, Position pos, int delayMs) {
player.setQueuedTeleport(eventExecutor.submit(() -> {
try {
Thread.sleep(delayMs);
transferPlayerToScene(player, sceneId, pos);
} catch (InterruptedException e) {
Grasscutter.getLogger().trace("queueTransferPlayerToScene: teleport to scene {} is interrupted", sceneId);
}
}));
}

public boolean transferPlayerToScene(Player player, int sceneId, Position pos) {
return this.transferPlayerToScene(player, sceneId, TeleportType.INTERNAL, null, pos);
}
Expand Down Expand Up @@ -381,6 +405,16 @@ public boolean transferPlayerToScene(
}

public boolean transferPlayerToScene(Player player, TeleportProperties teleportProperties) {
// If a queued teleport already exists, cancel it. This prevents the player from
// becoming stranded in a dungeon due to quitting it by teleporting to a map waypoint.
synchronized (player) {
var queuedTeleport = player.getQueuedTeleport();
if (queuedTeleport != null) {
player.setQueuedTeleport(null);
queuedTeleport.cancel(true);
}
}

// Check if the teleport properties are valid.
if (teleportProperties.getTeleportTo() == null)
teleportProperties.setTeleportTo(player.getPosition());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package emu.grasscutter.server.packet.recv;

import emu.grasscutter.net.packet.*;
import emu.grasscutter.net.proto.DungeonDieOptionReqOuterClass.DungeonDieOptionReq;
import emu.grasscutter.net.proto.PlayerDieOptionOuterClass.PlayerDieOption;
import emu.grasscutter.server.game.GameSession;

@Opcodes(PacketOpcodes.DungeonDieOptionReq)
public class HandlerDungeonDieOptionReq extends PacketHandler {

@Override
public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
DungeonDieOptionReq req = DungeonDieOptionReq.parseFrom(payload);
var dieOption = req.getDieOption();
// TODO Handle other die options
if (req.getIsQuitImmediately()) {
session.getPlayer().getServer().getDungeonSystem().exitDungeon(session.getPlayer());
}
session.getPlayer().sendPacket(new BasePacket(PacketOpcodes.DungeonDieOptionRsp));
}
}

Loading