feat: support multiplayer mode in teapot (#2317)

This commit is contained in:
hamusuke
2023-08-30 10:12:21 +09:00
committed by GitHub
parent 667008ecf1
commit 8563d4b574
51 changed files with 1740 additions and 844 deletions

View File

@@ -0,0 +1,10 @@
package emu.grasscutter.game.home;
import emu.grasscutter.game.CoopRequest;
import emu.grasscutter.game.player.Player;
public class EnterHomeRequest extends CoopRequest {
public EnterHomeRequest(Player requester) {
super(requester);
}
}

View File

@@ -4,31 +4,40 @@ import dev.morphia.annotations.*;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.GameData;
import emu.grasscutter.data.excels.HomeWorldLevelData;
import emu.grasscutter.data.excels.scene.SceneData;
import emu.grasscutter.database.DatabaseHelper;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.props.SceneType;
import emu.grasscutter.server.packet.send.*;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Data;
import lombok.experimental.FieldDefaults;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Data;
import lombok.experimental.FieldDefaults;
@Entity(value = "homes", useDiscriminator = false)
@Data
@FieldDefaults(level = AccessLevel.PRIVATE)
@Builder(builderMethodName = "of")
public class GameHome {
@Id String id;
public static final Set<Integer> HOME_SCENE_IDS = GameData.getSceneDataMap().values().stream()
.filter(sceneData -> sceneData.getSceneType() == SceneType.SCENE_HOME_WORLD || sceneData.getSceneType() == SceneType.SCENE_HOME_ROOM)
.map(SceneData::getId).collect(Collectors.toUnmodifiableSet());
@Id
String id;
@Indexed(options = @IndexOptions(unique = true))
long ownerUid;
@Transient Player player;
@Transient
Player player;
int level;
int exp;
@@ -49,13 +58,17 @@ public class GameHome {
return home;
}
public static boolean doesHomeExist(int uid) {
return DatabaseHelper.getHomeByUid(uid) != null;
}
public static GameHome create(Integer uid) {
return GameHome.of()
.ownerUid(uid)
.level(1)
.sceneMap(new ConcurrentHashMap<>())
.unlockedHomeBgmList(new HashSet<>())
.build();
.ownerUid(uid)
.level(1)
.sceneMap(new ConcurrentHashMap<>())
.unlockedHomeBgmList(new HashSet<>())
.build();
}
public void save() {
@@ -64,19 +77,19 @@ public class GameHome {
public HomeSceneItem getHomeSceneItem(int sceneId) {
return sceneMap.computeIfAbsent(
sceneId,
e -> {
var defaultItem = GameData.getHomeworldDefaultSaveData().get(sceneId);
if (defaultItem != null) {
Grasscutter.getLogger()
.info("Set player {} home {} to initial setting", ownerUid, sceneId);
return HomeSceneItem.parseFrom(defaultItem, sceneId);
} else {
// Realm res missing bricks account, use default realm data to allow main house
defaultItem = GameData.getHomeworldDefaultSaveData().get(2001);
return HomeSceneItem.parseFrom(defaultItem, sceneId);
}
});
sceneId,
e -> {
var defaultItem = GameData.getHomeworldDefaultSaveData().get(sceneId);
if (defaultItem != null) {
Grasscutter.getLogger()
.info("Set player {} home {} to initial setting", ownerUid, sceneId);
return HomeSceneItem.parseFrom(defaultItem, sceneId);
} else {
// Realm res missing bricks account, use default realm data to allow main house
defaultItem = GameData.getHomeworldDefaultSaveData().get(2001);
return HomeSceneItem.parseFrom(defaultItem, sceneId);
}
});
}
public void onOwnerLogin(Player player) {
@@ -130,9 +143,9 @@ public class GameHome {
private Set<Integer> getDefaultUnlockedHomeBgmIds() {
return GameData.getHomeWorldBgmDataMap().int2ObjectEntrySet().stream()
.filter(e -> e.getValue().isDefaultUnlock())
.map(Int2ObjectMap.Entry::getIntKey)
.collect(Collectors.toUnmodifiableSet());
.filter(e -> e.getValue().isDefaultUnlock())
.map(Int2ObjectMap.Entry::getIntKey)
.collect(Collectors.toUnmodifiableSet());
}
// Same as Player.java addExpDirectly
@@ -171,7 +184,7 @@ public class GameHome {
// Ensure next update is at top of the hour
nextUpdateTime =
(int) ZonedDateTime.now().plusHours(1).truncatedTo(ChronoUnit.HOURS).toEpochSecond();
(int) ZonedDateTime.now().plusHours(1).truncatedTo(ChronoUnit.HOURS).toEpochSecond();
// Get resources
var hourlyResources = getComfortResources(player);
@@ -194,42 +207,42 @@ public class GameHome {
// Outdoors avatars
sceneMap
.get(player.getCurrentRealmId() + 2000)
.getBlockItems()
.forEach(
(i, e) -> {
e.getDeployNPCList()
.forEach(
id -> {
invitedAvatars.add(id.getAvatarId());
});
});
.get(player.getCurrentRealmId() + 2000)
.getBlockItems()
.forEach(
(i, e) -> {
e.getDeployNPCList()
.forEach(
id -> {
invitedAvatars.add(id.getAvatarId());
});
});
// Check as realm 5 inside is not in defaults and will be null
if (Objects.nonNull(sceneMap.get(player.getCurrentRealmId() + 2200))) {
// Indoors avatars
sceneMap
.get(player.getCurrentRealmId() + 2200)
.getBlockItems()
.forEach(
(i, e) -> {
e.getDeployNPCList()
.forEach(
id -> {
invitedAvatars.add(id.getAvatarId());
});
});
.get(player.getCurrentRealmId() + 2200)
.getBlockItems()
.forEach(
(i, e) -> {
e.getDeployNPCList()
.forEach(
id -> {
invitedAvatars.add(id.getAvatarId());
});
});
}
// Add exp to all avatars
invitedAvatars.forEach(
id -> {
var avatar = player.getAvatars().getAvatarById(id);
player
.getServer()
.getInventorySystem()
.upgradeAvatarFetterLevel(player, avatar, storedFetterExp);
});
id -> {
var avatar = player.getAvatars().getAvatarById(id);
player
.getServer()
.getInventorySystem()
.upgradeAvatarFetterLevel(player, avatar, storedFetterExp);
});
storedFetterExp = 0;
save();
@@ -253,7 +266,7 @@ public class GameHome {
storeResources(player, 0, 0);
lastUpdatedTime = clientTime;
nextUpdateTime =
(int) ZonedDateTime.now().plusHours(1).truncatedTo(ChronoUnit.HOURS).toEpochSecond();
(int) ZonedDateTime.now().plusHours(1).truncatedTo(ChronoUnit.HOURS).toEpochSecond();
save();
// Send packet

View File

@@ -0,0 +1,162 @@
package emu.grasscutter.game.home;
import emu.grasscutter.game.entity.EntityTeam;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.world.Scene;
import emu.grasscutter.game.world.World;
import emu.grasscutter.net.packet.BasePacket;
import emu.grasscutter.net.proto.ChatInfoOuterClass;
import emu.grasscutter.server.game.GameServer;
import emu.grasscutter.server.packet.send.PacketDelTeamEntityNotify;
import emu.grasscutter.server.packet.send.PacketPlayerChatNotify;
import lombok.Getter;
import java.util.List;
public class HomeWorld extends World {
@Getter
private final GameHome home;
public HomeWorld(GameServer server, Player owner) {
super(server, owner);
this.home = owner.isOnline() ? owner.getHome() : GameHome.getByUid(owner.getUid());
server.registerHomeWorld(this);
}
@Override
public synchronized void addPlayer(Player player) {
// Check if player already in
if (this.getPlayers().contains(player)) {
return;
}
// Remove player from prev world
if (player.getWorld() != null) {
player.getWorld().removePlayer(player);
}
// Register
player.setWorld(this);
this.getPlayers().add(player);
// Set player variables
if (this.getHost().equals(player)) {
player.setPeerId(1);
this.getGuests().forEach(player1 -> player1.setPeerId(player1.getPeerId() + 1));
} else {
player.setPeerId(this.getNextPeerId());
}
player.getTeamManager().setEntity(new EntityTeam(player));
// Copy main team to multiplayer team
if (this.isMultiplayer()) {
player
.getTeamManager()
.getMpTeam()
.copyFrom(
player.getTeamManager().getCurrentSinglePlayerTeamInfo(),
player.getTeamManager().getMaxTeamSize());
player.getTeamManager().setCurrentCharacterIndex(0);
if (!player.equals(this.getHost())) {
this.broadcastPacket(
new PacketPlayerChatNotify(
player,
0,
ChatInfoOuterClass.ChatInfo.SystemHint.newBuilder()
.setType(ChatInfoOuterClass.ChatInfo.SystemHintType.SYSTEM_HINT_TYPE_CHAT_ENTER_WORLD.getNumber())
.build()));
}
}
// Add to scene
var scene = this.getSceneById(player.getSceneId());
scene.addPlayer(player);
// Info packet for other players
if (this.getPlayers().size() > 1) {
this.updatePlayerInfos(player);
}
}
@Override
public synchronized void removePlayer(Player player) {
// Remove team entities
this.broadcastPacket(
new PacketDelTeamEntityNotify(
player.getSceneId(),
this.getPlayers().stream()
.map(
p ->
p.getTeamManager().getEntity() == null
? 0
: p.getTeamManager().getEntity().getId())
.toList()));
// Deregister
this.getPlayers().remove(player);
player.setWorld(null);
// Remove from scene
Scene scene = this.getSceneById(player.getSceneId());
scene.removePlayer(player);
// Info packet for other players
if (this.getPlayers().size() > 0) {
this.updatePlayerInfos(player);
}
this.broadcastPacket(
new PacketPlayerChatNotify(
player,
0,
ChatInfoOuterClass.ChatInfo.SystemHint.newBuilder()
.setType(ChatInfoOuterClass.ChatInfo.SystemHintType.SYSTEM_HINT_TYPE_CHAT_LEAVE_WORLD.getNumber())
.build()));
}
@Override
public int getNextPeerId() {
return this.getPlayers().size() + 1;
}
@Override
public synchronized void setHost(Player host) {
super.setHost(host);
}
@Override
public final boolean isMultiplayer() {
return true;
}
@Override
public final boolean isPaused() {
return false;
}
@Override
public final boolean isTimeLocked() {
return false;
}
public int getOwnerUid() {
return this.getHost().getUid();
}
public List<Player> getGuests() {
return this.getPlayers().stream().filter(player -> !player.equals(this.getHost())).toList();
}
public boolean isInHome(Player player) {
return this.getPlayers().contains(player);
}
public void sendPacketToHostIfOnline(BasePacket basePacket) {
if (this.getHost().isOnline()) {
this.getHost().sendPacket(basePacket);
}
}
}

View File

@@ -0,0 +1,187 @@
package emu.grasscutter.game.home;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.props.EnterReason;
import emu.grasscutter.game.world.Position;
import emu.grasscutter.game.world.World;
import emu.grasscutter.game.world.data.TeleportProperties;
import emu.grasscutter.net.proto.EnterTypeOuterClass;
import emu.grasscutter.net.proto.OtherPlayerEnterHomeNotifyOuterClass;
import emu.grasscutter.net.proto.PlayerApplyEnterHomeResultNotifyOuterClass;
import emu.grasscutter.net.proto.RetcodeOuterClass;
import emu.grasscutter.server.event.player.PlayerEnterHomeEvent;
import emu.grasscutter.server.event.player.PlayerLeaveHomeEvent;
import emu.grasscutter.server.event.player.PlayerTeleportEvent;
import emu.grasscutter.server.game.BaseGameSystem;
import emu.grasscutter.server.game.GameServer;
import emu.grasscutter.server.packet.send.*;
public class HomeWorldMPSystem extends BaseGameSystem {
public HomeWorldMPSystem(GameServer server) {
super(server);
}
public void sendEnterHomeRequest(Player requester, int ownerUid) {
var owner = getServer().getPlayerByUid(ownerUid);
if (owner == null) {
requester.sendPacket(new PacketPlayerApplyEnterHomeResultNotify(ownerUid, "", false, PlayerApplyEnterHomeResultNotifyOuterClass.PlayerApplyEnterHomeResultNotify.Reason.OPEN_STATE_NOT_OPEN));
requester.sendPacket(new PacketTryEnterHomeRsp());
return;
}
if (owner.getRealmList() == null) {
requester.sendPacket(new PacketPlayerApplyEnterHomeResultNotify(ownerUid, "", false, PlayerApplyEnterHomeResultNotifyOuterClass.PlayerApplyEnterHomeResultNotify.Reason.OPEN_STATE_NOT_OPEN));
requester.sendPacket(new PacketTryEnterHomeRsp());
return;
}
var request = owner.getEnterHomeRequests().get(requester.getUid());
if (request != null && !request.isExpired()) {
return;
}
if (owner.isInEditMode()) {
requester.sendPacket(new PacketPlayerApplyEnterHomeResultNotify(ownerUid, owner.getNickname(), false, PlayerApplyEnterHomeResultNotifyOuterClass.PlayerApplyEnterHomeResultNotify.Reason.HOST_IN_EDIT_MODE));
requester.sendPacket(new PacketTryEnterHomeRsp());
return;
}
request = new EnterHomeRequest(requester);
owner.getEnterHomeRequests().put(requester.getUid(), request);
owner.sendPacket(new PacketPlayerApplyEnterHomeNotify(requester));
}
public void acceptEnterHomeRequest(Player owner, int requesterUid, boolean isAgreed) {
var request = owner.getEnterHomeRequests().get(requesterUid);
if (request == null || request.isExpired()) {
return;
}
var requester = request.getRequester();
owner.getEnterHomeRequests().remove(requesterUid);
if (requester.getWorld().isMultiplayer()) {
requester.sendPacket(new PacketPlayerApplyEnterHomeResultNotify(owner.getUid(), owner.getNickname(), false, PlayerApplyEnterHomeResultNotifyOuterClass.PlayerApplyEnterHomeResultNotify.Reason.SYSTEM_JUDGE));
requester.sendPacket(new PacketTryEnterHomeRsp());
return;
}
requester.sendPacket(new PacketPlayerApplyEnterHomeResultNotify(owner.getUid(), owner.getNickname(), isAgreed, PlayerApplyEnterHomeResultNotifyOuterClass.PlayerApplyEnterHomeResultNotify.Reason.PLAYER_JUDGE));
if (!isAgreed) {
requester.sendPacket(new PacketTryEnterHomeRsp());
return;
}
this.enterHome(requester, owner);
}
public void enterHome(Player requester, Player owner) {
if (requester.getWorld().isMultiplayer()) {
return;
}
if (owner.getRealmList() == null) {
// should never happen
requester.sendPacket(new PacketTryEnterHomeRsp(RetcodeOuterClass.Retcode.RET_HOME_NOT_FOUND_IN_MEM_VALUE, owner.getUid()));
return;
}
var world = this.server.getHomeWorldOrCreate(owner);
var targetHome = world.getHome();
var event = new PlayerEnterHomeEvent(requester, owner, targetHome);
event.call();
if (event.isCanceled()) {
requester.sendPacket(new PacketTryEnterHomeRsp(RetcodeOuterClass.Retcode.RET_HOME_OWNER_REFUSE_TO_ENTER_HOME_VALUE, owner.getUid()));
return;
}
if (owner.isInEditMode()) {
requester.sendPacket(new PacketTryEnterHomeRsp(RetcodeOuterClass.Retcode.RET_HOME_CANT_ENTER_BY_IN_EDIT_MODE_VALUE, owner.getUid()));
return;
}
int realmId = 2000 + owner.getCurrentRealmId();
targetHome.getHomeSceneItem(realmId);
targetHome.save();
var pos = world.getSceneById(realmId).getScriptManager().getConfig().born_pos;
requester.getPrevPosForHome().set(requester.getPosition());
requester.setCurHomeWorld(world);
requester.setPrevScene(requester.getSceneId());
world.addPlayer(requester, realmId);
requester.setSceneId(realmId);
requester.getPosition().set(pos);
requester.sendPacket(new PacketPlayerEnterSceneNotify(requester, owner.getUid(), TeleportProperties.builder().sceneId(realmId).enterReason(EnterReason.EnterHome).teleportTo(pos).teleportType(PlayerTeleportEvent.TeleportType.INTERNAL).build(), !requester.equals(owner)));
requester.sendPacket(new PacketTryEnterHomeRsp(owner.getUid()));
requester.setHasSentInitPacketInHome(false);
world.getPlayers().stream()
.filter(player -> !player.equals(requester))
.forEach(player -> player.sendPacket(new PacketPlayerPreEnterMpNotify(requester)));
}
public boolean leaveCoop(Player player, int prevScene) {
return this.leaveCoop(player, prevScene, player.getPrevPosForHome());
}
public boolean leaveCoop(Player player, int prevScene, Position pos) {
// Make sure everyone's scene is loaded
for (var p : player.getWorld().getPlayers()) {
if (p.getSceneLoadState() != Player.SceneLoadState.LOADED) {
return false;
}
}
// Event
var event = new PlayerLeaveHomeEvent(player, player.getCurHomeWorld().getHost(), player.getCurHomeWorld().getHome(), PlayerLeaveHomeEvent.Reason.PLAYER_LEAVE);
event.call();
player.getPosition().set(pos);
var world = new World(player);
world.addPlayer(player, prevScene);
player.getCurHomeWorld().sendPacketToHostIfOnline(new PacketOtherPlayerEnterOrLeaveHomeNotify(player, OtherPlayerEnterHomeNotifyOuterClass.OtherPlayerEnterHomeNotify.Reason.LEAVE));
player.setCurHomeWorld(this.server.getHomeWorldOrCreate(player));
player.sendPacket(new PacketPlayerEnterSceneNotify(player, EnterTypeOuterClass.EnterType.ENTER_TYPE_BACK, EnterReason.TeamBack, prevScene, pos));
return true;
}
public boolean kickPlayerFromHome(Player owner, int targetUid) {
// Make sure player's world is multiplayer and that player is owner
if (!owner.getCurHomeWorld().getHost().equals(owner)) {
return false;
}
// Get victim and sanity checks
var victim = owner.getServer().getPlayerByUid(targetUid);
if (victim == null || owner.equals(victim)) {
return false;
}
// Make sure victim's scene has loaded
if (victim.getSceneLoadState() != Player.SceneLoadState.LOADED) {
return false;
}
// Event
var event = new PlayerLeaveHomeEvent(victim, owner, victim.getCurHomeWorld().getHome(), PlayerLeaveHomeEvent.Reason.KICKED);
event.call();
// Kick
victim.getPosition().set(victim.getPrevPosForHome());
var world = new World(victim);
world.addPlayer(victim, 3);
victim.getCurHomeWorld().sendPacketToHostIfOnline(new PacketOtherPlayerEnterOrLeaveHomeNotify(victim, OtherPlayerEnterHomeNotifyOuterClass.OtherPlayerEnterHomeNotify.Reason.LEAVE));
victim.setCurHomeWorld(this.server.getHomeWorldOrCreate(victim));
victim.sendPacket(new PacketPlayerEnterSceneNotify(victim, EnterTypeOuterClass.EnterType.ENTER_TYPE_BACK, EnterReason.TeamKick, victim.getScene().getId(), victim.getPrevPosForHome()));
return true;
}
}