Fix some revives; improve dungeon exit flow (#2409)

This commit is contained in:
longfruit 2023-10-25 19:27:48 -07:00 committed by GitHub
parent 837e30e04b
commit f86259a430
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 145 additions and 31 deletions

View File

@ -7,6 +7,7 @@ import emu.grasscutter.data.binout.*;
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;
@ -562,6 +563,14 @@ public final class AbilityManager extends BasePlayerManager {
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));

View File

@ -168,13 +168,21 @@ public final class DungeonSystem extends BaseGameSystem {
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) {

View File

@ -67,6 +67,11 @@ public class EntityAvatar extends GameEntity {
}
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

View File

@ -174,13 +174,7 @@ public abstract class GameEntity {
}
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
@ -194,6 +188,17 @@ public abstract class GameEntity {
}
}
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}.
*
@ -333,6 +338,8 @@ public abstract class GameEntity {
if (entityController != null) {
entityController.onDie(this, getLastAttackType());
}
this.isDead = true;
}
/** Invoked when a global ability value is updated. */

View File

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

View File

@ -425,6 +425,30 @@ public final class TeamManager extends BasePlayerDataManager {
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) {
@ -700,15 +724,16 @@ public final class TeamManager extends BasePlayerDataManager {
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) {
@ -810,20 +835,13 @@ public final class TeamManager extends BasePlayerDataManager {
// 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.
@ -831,10 +849,6 @@ public final class TeamManager extends BasePlayerDataManager {
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);
}
}
@ -842,6 +856,20 @@ public final class TeamManager extends BasePlayerDataManager {
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) {

View File

@ -2,6 +2,7 @@ package emu.grasscutter.game.world;
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.*;
@ -19,8 +20,10 @@ import emu.grasscutter.server.event.player.PlayerTeleportEvent.TeleportType;
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;
@ -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);
}
@ -311,6 +324,17 @@ public class World implements Iterable<Player> {
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);
}
@ -381,6 +405,16 @@ public class World implements Iterable<Player> {
}
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());

View File

@ -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));
}
}