feat: nPC
This commit is contained in:
parent
cd5689b4a6
commit
f1f5e204a2
|
|
@ -36,6 +36,7 @@ import net.minecraft.network.chat.Style;
|
|||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.GameType;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraftforge.common.util.FakePlayerFactory;
|
||||
|
|
@ -46,6 +47,7 @@ import top.r3944realms.eroticdungeongame.config.EDGConfig;
|
|||
import top.r3944realms.eroticdungeongame.content.block.ISeatBlock;
|
||||
import top.r3944realms.eroticdungeongame.content.block.blockentity.BaseSeatBlockEntity;
|
||||
import top.r3944realms.eroticdungeongame.content.entity.SeatEntity;
|
||||
import top.r3944realms.eroticdungeongame.content.entity.npc.NPCServerPlayer;
|
||||
import top.r3944realms.eroticdungeongame.content.item.DeviceKeyItem;
|
||||
import top.r3944realms.eroticdungeongame.content.item.FilterItem;
|
||||
import top.r3944realms.eroticdungeongame.content.util.FurnitureHelper;
|
||||
|
|
@ -121,6 +123,13 @@ public class EDGCommand extends SimpleHelpCommand {
|
|||
Commands.literal("norp")
|
||||
.executes(this::edg$norp)
|
||||
)
|
||||
.then(
|
||||
Commands.literal("npc")
|
||||
.then(
|
||||
Commands.argument("name", StringArgumentType.word())
|
||||
.executes(this::edg$npc)
|
||||
)
|
||||
)
|
||||
.then(
|
||||
Commands.literal("device")
|
||||
.requires(source -> source.hasPermission(2))
|
||||
|
|
@ -496,6 +505,16 @@ public class EDGCommand extends SimpleHelpCommand {
|
|||
|
||||
);
|
||||
}
|
||||
private int edg$npc(@NotNull CommandContext<CommandSourceStack> context) throws CommandSyntaxException {
|
||||
//todo: 完善下内容
|
||||
CommandSourceStack source = context.getSource();
|
||||
ServerPlayer player = source.getPlayerOrException();
|
||||
String name = StringArgumentType.getString(context, "name");
|
||||
if (name.length() < 16) {
|
||||
NPCServerPlayer.createNPC(name, source.getServer(), player.getX(), player.getY(), player.getZ(), source.getRotation().x, source.getRotation().y, source.getLevel().dimension(), GameType.SURVIVAL, false);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
private int edg$norp(@NotNull CommandContext<CommandSourceStack> context) throws CommandSyntaxException {
|
||||
//todo: 完善下内容
|
||||
CommandSourceStack source = context.getSource();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Copyright 2025-2026 R3944Realms
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.r3944realms.eroticdungeongame.content.entity.npc;
|
||||
|
||||
public interface INPCPlayer {
|
||||
}
|
||||
|
|
@ -0,0 +1,437 @@
|
|||
/*
|
||||
* Copyright 2025-2026 R3944Realms
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.r3944realms.eroticdungeongame.content.entity.npc;
|
||||
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.mojang.authlib.GameProfile;
|
||||
import com.mojang.logging.LogUtils;
|
||||
import com.mojang.serialization.Dynamic;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.core.LayeredRegistryAccess;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.core.RegistrySynchronization;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.NbtOps;
|
||||
import net.minecraft.network.Connection;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
import net.minecraft.network.chat.*;
|
||||
import net.minecraft.network.protocol.Packet;
|
||||
import net.minecraft.network.protocol.game.*;
|
||||
import net.minecraft.network.protocol.status.ServerStatus;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.PlayerAdvancements;
|
||||
import net.minecraft.server.RegistryLayer;
|
||||
import net.minecraft.server.ServerScoreboard;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.server.network.ServerGamePacketListenerImpl;
|
||||
import net.minecraft.server.players.GameProfileCache;
|
||||
import net.minecraft.server.players.PlayerList;
|
||||
import net.minecraft.stats.ServerStatsCounter;
|
||||
import net.minecraft.stats.Stats;
|
||||
import net.minecraft.tags.TagNetworkSerialization;
|
||||
import net.minecraft.world.effect.MobEffectInstance;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.EntityType;
|
||||
import net.minecraft.world.flag.FeatureFlags;
|
||||
import net.minecraft.world.level.GameRules;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.biome.BiomeManager;
|
||||
import net.minecraft.world.level.border.WorldBorder;
|
||||
import net.minecraft.world.level.dimension.DimensionType;
|
||||
import net.minecraft.world.level.storage.LevelData;
|
||||
import net.minecraft.world.scores.Objective;
|
||||
import net.minecraft.world.scores.PlayerTeam;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.slf4j.Logger;
|
||||
import top.r3944realms.eroticdungeongame.core.network.EDGNetworkHandler;
|
||||
import top.r3944realms.eroticdungeongame.core.network.toClient.NPCInfoUpdatePacket;
|
||||
import top.r3944realms.eroticdungeongame.core.storage.NPCDataStorage;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
public class NPCPlayerList {
|
||||
private static final Logger LOGGER = LogUtils.getLogger();
|
||||
private static final int SEND_PLAYER_INFO_INTERVAL = 600;
|
||||
private static final SimpleDateFormat BAN_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd 'at' HH:mm:ss z");
|
||||
private final LayeredRegistryAccess<RegistryLayer> registries;
|
||||
private final RegistryAccess.Frozen synchronizedRegistries;
|
||||
private final NPCDataStorage npcIo;
|
||||
private final MinecraftServer server;
|
||||
private final List<NPCServerPlayer> npcs = new ArrayList<>();
|
||||
private final Map<UUID, NPCServerPlayer> npcsByUUID = Maps.newHashMap();
|
||||
private int sendAllNPCInfoIn;
|
||||
private final List<NPCServerPlayer> npcsView = java.util.Collections.unmodifiableList(npcs);
|
||||
public static final Component CHAT_FILTERED_FULL = Component.translatable("chat.filtered_full");
|
||||
public NPCPlayerList(MinecraftServer server, LayeredRegistryAccess<RegistryLayer> registries, NPCDataStorage npcDataStorage, int maxPlayers) {
|
||||
this.server = server;
|
||||
this.registries = registries;
|
||||
this.synchronizedRegistries = (new RegistryAccess.ImmutableRegistryAccess(RegistrySynchronization.networkedRegistries(registries))).freeze();
|
||||
this.npcIo = npcDataStorage;
|
||||
}
|
||||
public void placeNewNPC(Connection netManager, NPCServerPlayer npc) {
|
||||
GameProfile gameprofile = npc.getGameProfile();
|
||||
GameProfileCache gameprofilecache = this.server.getProfileCache();
|
||||
String s;
|
||||
if (gameprofilecache != null) {
|
||||
Optional<GameProfile> optional = gameprofilecache.get(gameprofile.getId());
|
||||
s = optional.map(GameProfile::getName).orElse(gameprofile.getName());
|
||||
gameprofilecache.add(gameprofile);
|
||||
} else {
|
||||
s = gameprofile.getName();
|
||||
}
|
||||
CompoundTag compoundtag = this.load(npc);
|
||||
//noinspection deprecation
|
||||
ResourceKey<Level> resourcekey = compoundtag != null ? DimensionType.parseLegacy(new Dynamic<>(NbtOps.INSTANCE, compoundtag.get("Dimension"))).resultOrPartial(LOGGER::error).orElse(Level.OVERWORLD) : Level.OVERWORLD;
|
||||
ServerLevel serverlevel = this.server.getLevel(resourcekey);
|
||||
ServerLevel serverlevel1;
|
||||
if (serverlevel == null) {
|
||||
LOGGER.warn("Unknown respawn dimension {}, defaulting to overworld", resourcekey);
|
||||
serverlevel1 = this.server.overworld();
|
||||
} else {
|
||||
serverlevel1 = serverlevel;
|
||||
}
|
||||
|
||||
npc.setServerLevel(serverlevel1);
|
||||
String s1 = "local";
|
||||
//noinspection ConstantValue
|
||||
if (netManager.getRemoteAddress() != null) {
|
||||
s1 = net.minecraftforge.network.DualStackUtils.getAddressString(netManager.getRemoteAddress());
|
||||
}
|
||||
LOGGER.info("[NPC] {}[{}] logged in with entity id {} at ({}, {}, {})", npc.getName().getString(), s1, npc.getId(), npc.getX(), npc.getY(), npc.getZ());
|
||||
LevelData leveldata = serverlevel1.getLevelData();
|
||||
npc.loadGameTypes(compoundtag);
|
||||
ServerGamePacketListenerImpl servergamepacketlistenerimpl = new ServerGamePacketListenerImpl(this.server, netManager, npc);
|
||||
net.minecraftforge.network.NetworkHooks.sendMCRegistryPackets(netManager, "PLAY_TO_CLIENT");
|
||||
GameRules gamerules = serverlevel1.getGameRules();
|
||||
boolean flag = gamerules.getBoolean(GameRules.RULE_DO_IMMEDIATE_RESPAWN);
|
||||
boolean flag1 = gamerules.getBoolean(GameRules.RULE_REDUCEDDEBUGINFO);
|
||||
servergamepacketlistenerimpl.send(new ClientboundLoginPacket(npc.getId(), leveldata.isHardcore(), npc.gameMode.getGameModeForPlayer(), npc.gameMode.getPreviousGameModeForPlayer(), this.server.levelKeys(), this.synchronizedRegistries, serverlevel1.dimensionTypeId(), serverlevel1.dimension(), BiomeManager.obfuscateSeed(serverlevel1.getSeed()), 0, 16, 16, flag1, !flag, serverlevel1.isDebug(), serverlevel1.isFlat(), npc.getLastDeathLocation(), npc.getPortalCooldown()));
|
||||
servergamepacketlistenerimpl.send(new ClientboundUpdateEnabledFeaturesPacket(FeatureFlags.REGISTRY.toNames(serverlevel1.enabledFeatures())));
|
||||
servergamepacketlistenerimpl.send(new ClientboundCustomPayloadPacket(ClientboundCustomPayloadPacket.BRAND, (new FriendlyByteBuf(Unpooled.buffer())).writeUtf(this.getServer().getServerModName())));
|
||||
servergamepacketlistenerimpl.send(new ClientboundChangeDifficultyPacket(leveldata.getDifficulty(), leveldata.isDifficultyLocked()));
|
||||
servergamepacketlistenerimpl.send(new ClientboundPlayerAbilitiesPacket(npc.getAbilities()));
|
||||
servergamepacketlistenerimpl.send(new ClientboundSetCarriedItemPacket(npc.getInventory().selected));
|
||||
// todo: 数据同步事件(对于NPC类) 可以搞个继承类复用这个事件
|
||||
// net.minecraftforge.common.MinecraftForge.EVENT_BUS.post(new net.minecraftforge.event.OnDatapackSyncEvent(this, npc));
|
||||
servergamepacketlistenerimpl.send(new ClientboundUpdateRecipesPacket(this.server.getRecipeManager().getRecipes()));
|
||||
servergamepacketlistenerimpl.send(new ClientboundUpdateTagsPacket(TagNetworkSerialization.serializeTagsToNetwork(this.registries)));
|
||||
this.sendPlayerPermissionLevel(npc);
|
||||
npc.getStats().markAllDirty();
|
||||
npc.getRecipeBook().sendInitialRecipeBook(npc);
|
||||
this.updateEntireScoreboard(serverlevel1.getScoreboard(), npc);
|
||||
this.server.invalidateStatus();
|
||||
MutableComponent mutablecomponent;
|
||||
if (npc.getGameProfile().getName().equalsIgnoreCase(s)) { //todo npc
|
||||
mutablecomponent = Component.translatable("multiplayer.player.joined", npc.getDisplayName());
|
||||
} else {
|
||||
mutablecomponent = Component.translatable("multiplayer.player.joined.renamed", npc.getDisplayName(), s);
|
||||
}
|
||||
|
||||
this.broadcastSystemMessage(mutablecomponent.withStyle(ChatFormatting.YELLOW), false);
|
||||
servergamepacketlistenerimpl.teleport(npc.getX(), npc.getY(), npc.getZ(), npc.getYRot(), npc.getXRot());
|
||||
ServerStatus serverstatus = this.server.getStatus();
|
||||
if (serverstatus != null) {
|
||||
npc.sendServerStatus(serverstatus);
|
||||
}
|
||||
//todo 实现网络传递
|
||||
EDGNetworkHandler.sendToPlayer(NPCInfoUpdatePacket.createNPCPlayerInitializing(this.npcs), npc);
|
||||
this.npcs.add(npc);
|
||||
this.npcsByUUID.put(npc.getUUID(), npc);
|
||||
EDGNetworkHandler.sendToAllPlayer(NPCInfoUpdatePacket.createNPCPlayerInitializing(List.of(npc)));
|
||||
this.sendLevelInfo(npc, serverlevel1);
|
||||
serverlevel1.addNewPlayer(npc);
|
||||
this.server.getCustomBossEvents().onPlayerConnect(npc);
|
||||
this.server.getServerResourcePack().ifPresent((serverResourcePackInfo) -> npc.sendTexturePack(serverResourcePackInfo.url(), serverResourcePackInfo.hash(), serverResourcePackInfo.isRequired(), serverResourcePackInfo.prompt()));
|
||||
|
||||
for(MobEffectInstance mobeffectinstance : npc.getActiveEffects()) {
|
||||
servergamepacketlistenerimpl.send(new ClientboundUpdateMobEffectPacket(npc.getId(), mobeffectinstance));
|
||||
}
|
||||
|
||||
if (compoundtag != null && compoundtag.contains("RootVehicle", 10)) {
|
||||
CompoundTag compoundtag1 = compoundtag.getCompound("RootVehicle");
|
||||
Entity entity1 = EntityType.loadEntityRecursive(compoundtag1.getCompound("Entity"), serverlevel1, (p_215603_) -> !serverlevel1.addWithUUID(p_215603_) ? null : p_215603_);
|
||||
if (entity1 != null) {
|
||||
UUID uuid;
|
||||
if (compoundtag1.hasUUID("Attach")) {
|
||||
uuid = compoundtag1.getUUID("Attach");
|
||||
} else {
|
||||
uuid = null;
|
||||
}
|
||||
|
||||
if (entity1.getUUID().equals(uuid)) {
|
||||
npc.startRiding(entity1, true);
|
||||
} else {
|
||||
for(Entity entity : entity1.getIndirectPassengers()) {
|
||||
if (entity.getUUID().equals(uuid)) {
|
||||
npc.startRiding(entity, true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!npc.isPassenger()) {
|
||||
LOGGER.warn("Couldn't reattach entity to player");
|
||||
entity1.discard();
|
||||
|
||||
for(Entity entity2 : entity1.getIndirectPassengers()) {
|
||||
entity2.discard();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
npc.initInventoryMenu();
|
||||
net.minecraftforge.event.ForgeEventFactory.firePlayerLoggedIn(npc);
|
||||
}
|
||||
public void sendPlayerPermissionLevel(@NotNull ServerPlayer player) {
|
||||
GameProfile gameprofile = player.getGameProfile();
|
||||
int i = this.server.getProfilePermissions(gameprofile);
|
||||
this.sendPlayerPermissionLevel(player, i);
|
||||
}
|
||||
|
||||
private void sendPlayerPermissionLevel(@NotNull ServerPlayer player, int permLevel) {
|
||||
byte b0;
|
||||
if (permLevel <= 0) {
|
||||
b0 = 24;
|
||||
} else if (permLevel >= 4) {
|
||||
b0 = 28;
|
||||
} else {
|
||||
b0 = (byte)(24 + permLevel);
|
||||
}
|
||||
|
||||
player.connection.send(new ClientboundEntityEventPacket(player, b0));
|
||||
|
||||
this.server.getCommands().sendCommands(player);
|
||||
}
|
||||
/**
|
||||
* Updates the time and weather for the given player to those of the given world
|
||||
*/
|
||||
public void sendLevelInfo(@NotNull ServerPlayer player, @NotNull ServerLevel level) {
|
||||
WorldBorder worldborder = this.server.overworld().getWorldBorder();
|
||||
player.connection.send(new ClientboundInitializeBorderPacket(worldborder));
|
||||
player.connection.send(new ClientboundSetTimePacket(level.getGameTime(), level.getDayTime(), level.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)));
|
||||
player.connection.send(new ClientboundSetDefaultSpawnPositionPacket(level.getSharedSpawnPos(), level.getSharedSpawnAngle()));
|
||||
if (level.isRaining()) {
|
||||
player.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.START_RAINING, 0.0F));
|
||||
player.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.RAIN_LEVEL_CHANGE, level.getRainLevel(1.0F)));
|
||||
player.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.THUNDER_LEVEL_CHANGE, level.getThunderLevel(1.0F)));
|
||||
}
|
||||
|
||||
}
|
||||
protected void updateEntireScoreboard(@NotNull ServerScoreboard scoreboard, ServerPlayer player) {
|
||||
Set<Objective> set = Sets.newHashSet();
|
||||
|
||||
for(PlayerTeam playerteam : scoreboard.getPlayerTeams()) {
|
||||
player.connection.send(ClientboundSetPlayerTeamPacket.createAddOrModifyPacket(playerteam, true));
|
||||
}
|
||||
|
||||
for(int i = 0; i < 19; ++i) {
|
||||
Objective objective = scoreboard.getDisplayObjective(i);
|
||||
if (objective != null && !set.contains(objective)) {
|
||||
for(Packet<?> packet : scoreboard.getStartTrackingPackets(objective)) {
|
||||
player.connection.send(packet);
|
||||
}
|
||||
|
||||
set.add(objective);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* Called during player login. Reads the player information from disk.
|
||||
*/
|
||||
@Nullable
|
||||
public CompoundTag load(@NotNull NPCServerPlayer player) {
|
||||
CompoundTag compoundtag = this.server.getWorldData().getLoadedPlayerTag();
|
||||
CompoundTag compoundtag1;
|
||||
if (this.server.isSingleplayerOwner(player.getGameProfile()) && compoundtag != null) {
|
||||
compoundtag1 = compoundtag;
|
||||
player.load(compoundtag);
|
||||
LOGGER.debug("loading single npc");
|
||||
// net.minecraftforge.event.ForgeEventFactory.firePlayerLoadingEvent(player, this.npcIo, player.getUUID().toString());
|
||||
} else {
|
||||
compoundtag1 = this.npcIo.load(player);
|
||||
}
|
||||
|
||||
return compoundtag1;
|
||||
}
|
||||
public void broadcastSystemMessage(Component message, boolean bypassHiddenChat) {
|
||||
this.broadcastSystemMessage(message, (p_215639_) -> message, bypassHiddenChat);
|
||||
}
|
||||
public void saveAll() {
|
||||
for (NPCServerPlayer npc : this.npcs) {
|
||||
this.save(npc);
|
||||
}
|
||||
}
|
||||
|
||||
protected void save(NPCServerPlayer npcServerPlayer) {
|
||||
this.npcIo.save(npcServerPlayer);
|
||||
}
|
||||
public void removeAll() {
|
||||
for (NPCServerPlayer npc : this.npcs) {
|
||||
npc.connection.disconnect(Component.translatable("multiplayer.disconnect.server_shutdown"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void remove(NPCServerPlayer npc) {
|
||||
net.minecraftforge.event.ForgeEventFactory.firePlayerLoggedOut(npc);
|
||||
ServerLevel serverlevel = npc.serverLevel();
|
||||
npc.awardStat(Stats.LEAVE_GAME);
|
||||
this.save(npc);
|
||||
if (npc.isPassenger()) {
|
||||
Entity entity = npc.getRootVehicle();
|
||||
if (entity.hasExactlyOnePlayerPassenger()) {
|
||||
LOGGER.debug("Removing player mount");
|
||||
npc.stopRiding();
|
||||
entity.getPassengersAndSelf().forEach((p_215620_) -> {
|
||||
p_215620_.setRemoved(Entity.RemovalReason.UNLOADED_WITH_PLAYER);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
npc.unRide();
|
||||
serverlevel.removePlayerImmediately(npc, Entity.RemovalReason.UNLOADED_WITH_PLAYER);
|
||||
npc.getAdvancements().stopListening();
|
||||
this.npcs.remove(npc);
|
||||
this.server.getCustomBossEvents().onPlayerDisconnect(npc);
|
||||
UUID uuid = npc.getUUID();
|
||||
ServerPlayer serverplayer = this.npcsByUUID.get(uuid);
|
||||
if (serverplayer == npc) {
|
||||
this.npcsByUUID.remove(uuid);
|
||||
}
|
||||
|
||||
this.broadcastAll(new ClientboundPlayerInfoRemovePacket(List.of(npc.getUUID())));
|
||||
}
|
||||
|
||||
public void broadcastSystemMessage(Component serverMessage, Function<ServerPlayer, Component> playerMessageFactory, boolean bypassHiddenChat) {
|
||||
this.server.sendSystemMessage(serverMessage);
|
||||
PlayerList playerList = server.getPlayerList();
|
||||
for(ServerPlayer serverplayer : playerList.getPlayers()) {
|
||||
Component component = playerMessageFactory.apply(serverplayer);
|
||||
if (component != null) {
|
||||
serverplayer.sendSystemMessage(component, bypassHiddenChat);
|
||||
}
|
||||
}
|
||||
for(ServerPlayer serverplayer : this.npcs) {
|
||||
Component component = playerMessageFactory.apply(serverplayer);
|
||||
if (component != null) {
|
||||
serverplayer.sendSystemMessage(component, bypassHiddenChat);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void tick() {
|
||||
if (++this.sendAllNPCInfoIn > 600) {
|
||||
EDGNetworkHandler.sendToAllPlayer(new NPCInfoUpdatePacket(EnumSet.of(NPCInfoUpdatePacket.Action.UPDATE_LATENCY), this.npcs));
|
||||
this.sendAllNPCInfoIn = 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void broadcastChatMessage(PlayerChatMessage message, @NotNull CommandSourceStack sender, ChatType.Bound boundChatType) {
|
||||
this.broadcastChatMessage(message, sender::shouldFilterMessageTo, sender.getPlayer(), boundChatType);
|
||||
}
|
||||
|
||||
public void broadcastChatMessage(PlayerChatMessage message, ServerPlayer sender, ChatType.Bound boundChatType) {
|
||||
this.broadcastChatMessage(message, sender::shouldFilterMessageTo, sender, boundChatType);
|
||||
}
|
||||
|
||||
private void broadcastChatMessage(PlayerChatMessage message, Predicate<ServerPlayer> shouldFilterMessageTo, @Nullable ServerPlayer sender, ChatType.Bound boundChatType) {
|
||||
boolean flag = this.verifyChatTrusted(message);
|
||||
this.server.logChatMessage(message.decoratedContent(), boundChatType, flag ? null : "Not Secure");
|
||||
OutgoingChatMessage outgoingchatmessage = OutgoingChatMessage.create(message);
|
||||
boolean flag1 = false;
|
||||
PlayerList playerList = server.getPlayerList();
|
||||
for(ServerPlayer serverplayer : playerList.getPlayers()) {
|
||||
boolean flag2 = shouldFilterMessageTo.test(serverplayer);
|
||||
serverplayer.sendChatMessage(outgoingchatmessage, flag2, boundChatType);
|
||||
flag1 |= flag2 && message.isFullyFiltered();
|
||||
}
|
||||
for(ServerPlayer serverplayer : this.npcs) {
|
||||
boolean flag2 = shouldFilterMessageTo.test(serverplayer);
|
||||
serverplayer.sendChatMessage(outgoingchatmessage, flag2, boundChatType);
|
||||
flag1 |= flag2 && message.isFullyFiltered();
|
||||
}
|
||||
|
||||
if (flag1 && sender != null) {
|
||||
sender.sendSystemMessage(CHAT_FILTERED_FULL);
|
||||
}
|
||||
|
||||
}
|
||||
public void broadcastAll(Packet<?> packet) {
|
||||
PlayerList playerList = server.getPlayerList();
|
||||
for(ServerPlayer serverplayer : playerList.getPlayers()) {
|
||||
serverplayer.connection.send(packet);
|
||||
}
|
||||
for(ServerPlayer serverplayer : this.npcs) {
|
||||
serverplayer.connection.send(packet);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void broadcastAll(Packet<?> packet, ResourceKey<Level> dimension) {
|
||||
PlayerList playerList = server.getPlayerList();
|
||||
for(ServerPlayer serverplayer : playerList.getPlayers()) {
|
||||
if (serverplayer.level().dimension() == dimension) {
|
||||
serverplayer.connection.send(packet);
|
||||
}
|
||||
}
|
||||
for(ServerPlayer serverplayer : this.npcs) {
|
||||
if (serverplayer.level().dimension() == dimension) {
|
||||
serverplayer.connection.send(packet);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private boolean verifyChatTrusted(@NotNull PlayerChatMessage message) {
|
||||
return message.hasSignature() && !message.hasExpiredServer(Instant.now());
|
||||
}
|
||||
|
||||
public List<NPCServerPlayer> getPlayers() {
|
||||
return this.npcsView; //Unmodifiable view
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public ServerPlayer getNPC(UUID npcUUID) {
|
||||
return this.npcsByUUID.get(npcUUID);
|
||||
}
|
||||
public void reloadResources() {
|
||||
this.broadcastAll(new ClientboundUpdateTagsPacket(TagNetworkSerialization.serializeTagsToNetwork(this.registries)));
|
||||
ClientboundUpdateRecipesPacket clientboundupdaterecipespacket = new ClientboundUpdateRecipesPacket(this.server.getRecipeManager().getRecipes());
|
||||
for(ServerPlayer serverplayer : this.npcs) {
|
||||
serverplayer.connection.send(clientboundupdaterecipespacket);
|
||||
serverplayer.getRecipeBook().sendInitialRecipeBook(serverplayer);
|
||||
}
|
||||
|
||||
}
|
||||
public MinecraftServer getServer() {
|
||||
return this.server;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright 2025-2026 R3944Realms
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.r3944realms.eroticdungeongame.content.entity.npc;
|
||||
|
||||
import com.mojang.authlib.GameProfile;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
import net.minecraft.client.player.RemotePlayer;
|
||||
|
||||
public class NPCRemotePlayer extends RemotePlayer implements INPCPlayer {
|
||||
public NPCRemotePlayer(ClientLevel clientLevel, GameProfile gameProfile) {
|
||||
super(clientLevel, gameProfile);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,203 @@
|
|||
/*
|
||||
* Copyright 2025-2026 R3944Realms
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.r3944realms.eroticdungeongame.content.entity.npc;
|
||||
|
||||
import com.mojang.authlib.GameProfile;
|
||||
import dev.dubhe.curtain.utils.Messenger;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.UUIDUtil;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.protocol.PacketFlow;
|
||||
import net.minecraft.network.protocol.game.ClientboundRotateHeadPacket;
|
||||
import net.minecraft.network.protocol.game.ClientboundTeleportEntityPacket;
|
||||
import net.minecraft.network.protocol.game.ServerboundClientCommandPacket;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.TickTask;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.server.players.GameProfileCache;
|
||||
import net.minecraft.world.damagesource.DamageSource;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.EquipmentSlot;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.food.FoodData;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.GameType;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.entity.SkullBlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import top.r3944realms.eroticdungeongame.EroticDungeon;
|
||||
import top.r3944realms.eroticdungeongame.core.network.NPCEmptyClientConnection;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
public class NPCServerPlayer extends ServerPlayer implements INPCPlayer {
|
||||
public Runnable fixStartingPosition = () -> {
|
||||
};
|
||||
public NPCServerPlayer(MinecraftServer server, ServerLevel level, GameProfile gameProfile) {
|
||||
super(server, level, gameProfile);
|
||||
}
|
||||
|
||||
public static @Nullable NPCServerPlayer createNPC(String username, @NotNull MinecraftServer server, double x, double y, double z, double yaw, double pitch, ResourceKey<Level> dimensionId, GameType gamemode, boolean isflying) {
|
||||
ServerLevel worldIn = server.getLevel(dimensionId);
|
||||
if (worldIn != null) {
|
||||
GameProfileCache.setUsesAuthentication(false);
|
||||
GameProfile gameProfile = null;
|
||||
try {
|
||||
GameProfileCache profileCache = server.getProfileCache();
|
||||
if (profileCache != null) {
|
||||
gameProfile = profileCache.get(username).orElse(null);
|
||||
}
|
||||
} finally {
|
||||
GameProfileCache.setUsesAuthentication(server.isDedicatedServer() && server.usesAuthentication());
|
||||
}
|
||||
try {
|
||||
if (gameProfile == null) {
|
||||
gameProfile = new GameProfile(UUIDUtil.createOfflinePlayerUUID(username), username);
|
||||
}
|
||||
|
||||
if (gameProfile.getProperties().containsKey("textures")) {
|
||||
AtomicReference<GameProfile> result = new AtomicReference<>();
|
||||
Objects.requireNonNull(result);
|
||||
SkullBlockEntity.updateGameprofile(gameProfile, result::set);
|
||||
gameProfile = result.get();
|
||||
}
|
||||
NPCServerPlayer instance = new NPCServerPlayer(server, worldIn, gameProfile);
|
||||
instance.fixStartingPosition = () -> {
|
||||
instance.moveTo(x, y, z, (float) yaw, (float) pitch);
|
||||
};
|
||||
server.getPlayerList().placeNewPlayer(new NPCEmptyClientConnection(PacketFlow.SERVERBOUND), instance);
|
||||
instance.teleportTo(worldIn, x, y, z, (float) yaw, (float) pitch);
|
||||
instance.setHealth(20.0F);
|
||||
instance.unsetRemoved();
|
||||
instance.setMaxUpStep(0.6F);
|
||||
instance.gameMode.changeGameModeForPlayer(gamemode);
|
||||
server.getPlayerList().broadcastAll(new ClientboundRotateHeadPacket(instance, (byte)((int)(instance.yHeadRot * 256.0F / 360.0F))), dimensionId);
|
||||
server.getPlayerList().broadcastAll(new ClientboundTeleportEntityPacket(instance), dimensionId);
|
||||
instance.entityData.set(DATA_PLAYER_MODE_CUSTOMISATION, (byte)127);
|
||||
instance.getAbilities().flying = isflying;
|
||||
return instance;
|
||||
} catch (Exception e) {
|
||||
EroticDungeon.getLogger().error("Failed to create NPC", e);
|
||||
}
|
||||
} else EroticDungeon.getLogger().error("Failed to create NPC because server({}) is null!", dimensionId);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public void onEquipItem(EquipmentSlot p_238393_, ItemStack p_238394_, ItemStack p_238395_) {
|
||||
if (!this.isUsingItem()) {
|
||||
super.onEquipItem(p_238393_, p_238394_, p_238395_);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void kill() {
|
||||
this.kill(Messenger.s("Killed"));
|
||||
}
|
||||
|
||||
public void kill(Component reason) {
|
||||
this.shakeOff();
|
||||
this.server.tell(new TickTask(this.server.getTickCount(), () -> {
|
||||
this.connection.onDisconnect(reason);
|
||||
}));
|
||||
}
|
||||
|
||||
private void shakeOff() {
|
||||
if (this.getVehicle() instanceof Player) {
|
||||
this.stopRiding();
|
||||
}
|
||||
|
||||
Iterator var1 = this.getIndirectPassengers().iterator();
|
||||
|
||||
while(var1.hasNext()) {
|
||||
Entity passenger = (Entity)var1.next();
|
||||
if (passenger instanceof Player) {
|
||||
passenger.stopRiding();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void die(@NotNull DamageSource damageSource) {
|
||||
this.shakeOff();
|
||||
super.die(damageSource);
|
||||
this.setHealth(20.0F);
|
||||
this.foodData = new FoodData();
|
||||
this.kill(this.getCombatTracker().getDeathMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addAdditionalSaveData(@NotNull CompoundTag compound) {
|
||||
super.addAdditionalSaveData(compound);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readAdditionalSaveData(@NotNull CompoundTag compound) {
|
||||
super.readAdditionalSaveData(compound);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull String getIpAddress() {
|
||||
return "127.0.0.1";
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Component getName() {
|
||||
return Component.literal("[NPC]").append(super.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick() {
|
||||
if (this.getServer() != null && this.getServer().getTickCount() % 10 == 0) {
|
||||
this.connection.resetPosition();
|
||||
((ServerLevel)this.level()).getChunkSource().move(this);
|
||||
}
|
||||
|
||||
try {
|
||||
super.tick();
|
||||
this.doTick();
|
||||
} catch (NullPointerException ignored) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public Entity changeDimension(@NotNull ServerLevel level) {
|
||||
super.changeDimension(level);
|
||||
if (this.wonGame) {
|
||||
ServerboundClientCommandPacket packet = new ServerboundClientCommandPacket(net.minecraft.network.protocol.game.ServerboundClientCommandPacket.Action.PERFORM_RESPAWN);
|
||||
this.connection.handleClientCommand(packet);
|
||||
}
|
||||
|
||||
if (this.connection.player.isChangingDimension()) {
|
||||
this.connection.player.hasChangedDimension();
|
||||
}
|
||||
|
||||
return this.connection.player;
|
||||
}
|
||||
|
||||
protected void checkFallDamage(double y, boolean onGround, @NotNull BlockState state, @NotNull BlockPos pos) {
|
||||
this.doCheckFallDamage(this.getX(), y, this.getZ(), onGround);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -16,16 +16,31 @@
|
|||
|
||||
package top.r3944realms.eroticdungeongame.core.network;
|
||||
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraftforge.network.NetworkDirection;
|
||||
import net.minecraftforge.network.NetworkRegistry;
|
||||
import net.minecraftforge.network.PacketDistributor;
|
||||
import net.minecraftforge.network.simple.SimpleChannel;
|
||||
import net.minecraftforge.server.ServerLifecycleHooks;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import top.r3944realms.eroticdungeongame.EroticDungeon;
|
||||
import top.r3944realms.eroticdungeongame.core.network.toClient.NPCInfoRemovePacket;
|
||||
import top.r3944realms.eroticdungeongame.core.network.toClient.NPCInfoUpdatePacket;
|
||||
import top.r3944realms.eroticdungeongame.core.network.toServer.LoveMachineConfigPacket;
|
||||
import top.r3944realms.eroticdungeongame.core.network.toServer.RequestQuitDevicePacket;
|
||||
import top.r3944realms.eroticdungeongame.util.IEDGMinecraftServer;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public class EDGNetworkHandler {
|
||||
public static final PacketDistributor<Void> ALL_PLAYER = new PacketDistributor<>((voidPacketDistributor, supplier) -> {
|
||||
MinecraftServer minecraftServer = ServerLifecycleHooks.getCurrentServer();
|
||||
if (minecraftServer instanceof IEDGMinecraftServer iedgMinecraftServer) {
|
||||
return packet -> iedgMinecraftServer.getNPCPlayerList().broadcastAll(packet);
|
||||
}
|
||||
return p -> {};
|
||||
}, NetworkDirection.PLAY_TO_CLIENT);
|
||||
public static final SimpleChannel CHANNEL = NetworkRegistry.newSimpleChannel(
|
||||
EroticDungeon.rl("main"),
|
||||
() -> EroticDungeon.ModInfo.VERSION,
|
||||
|
|
@ -44,8 +59,26 @@ public class EDGNetworkHandler {
|
|||
.decoder(RequestQuitDevicePacket::new)
|
||||
.consumerMainThread(RequestQuitDevicePacket::handle)
|
||||
.add();
|
||||
CHANNEL.messageBuilder(NPCInfoRemovePacket.class, getIndex(), NetworkDirection.PLAY_TO_CLIENT)
|
||||
.encoder(NPCInfoRemovePacket::write)
|
||||
.decoder(NPCInfoRemovePacket::new)
|
||||
.consumerMainThread(NPCInfoRemovePacket::handle)
|
||||
.add();
|
||||
CHANNEL.messageBuilder(NPCInfoUpdatePacket.class, getIndex(), NetworkDirection.PLAY_TO_CLIENT)
|
||||
.encoder(NPCInfoUpdatePacket::write)
|
||||
.decoder(NPCInfoUpdatePacket::new)
|
||||
.consumerMainThread(NPCInfoUpdatePacket::handle)
|
||||
.add();
|
||||
}
|
||||
public static int getIndex() {
|
||||
return cid.getAndIncrement();
|
||||
}
|
||||
|
||||
public static <MSG> void sendToPlayer(MSG message, ServerPlayer player){
|
||||
CHANNEL.send(PacketDistributor.PLAYER.with(() -> player), message);
|
||||
}
|
||||
|
||||
public static <MSG> void sendToAllPlayer(MSG message){
|
||||
CHANNEL.send(ALL_PLAYER.noArg(), message);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright 2025-2026 R3944Realms
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.r3944realms.eroticdungeongame.core.network;
|
||||
|
||||
import io.netty.channel.embedded.EmbeddedChannel;
|
||||
import net.minecraft.network.Connection;
|
||||
import net.minecraft.network.protocol.PacketFlow;
|
||||
import top.r3944realms.eroticdungeongame.util.IClientConnection;
|
||||
|
||||
public class NPCEmptyClientConnection extends Connection {
|
||||
public NPCEmptyClientConnection(PacketFlow packetFlow) {
|
||||
super(packetFlow);
|
||||
((IClientConnection)this).setChannel(new EmbeddedChannel());
|
||||
}
|
||||
|
||||
public void setReadOnly() {
|
||||
}
|
||||
|
||||
public void handleDisconnection() {
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright 2025-2026 R3944Realms
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.r3944realms.eroticdungeongame.core.network.toClient;
|
||||
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.multiplayer.ClientPacketListener;
|
||||
import net.minecraft.client.multiplayer.PlayerInfo;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
import net.minecraftforge.network.NetworkEvent;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import top.r3944realms.eroticdungeongame.util.IEDGClientPacketListener;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public record NPCInfoRemovePacket(List<UUID> profileIds) {
|
||||
public NPCInfoRemovePacket(@NotNull FriendlyByteBuf buf) {
|
||||
this(buf.readList(FriendlyByteBuf::readUUID));
|
||||
}
|
||||
|
||||
public void write(@NotNull FriendlyByteBuf buffer) {
|
||||
buffer.writeCollection(this.profileIds, FriendlyByteBuf::writeUUID);
|
||||
}
|
||||
|
||||
|
||||
public static void handle(NPCInfoRemovePacket packet, @NotNull Supplier<NetworkEvent.Context> contextSupplier) {
|
||||
NetworkEvent.Context context = contextSupplier.get();
|
||||
context.enqueueWork(() -> {
|
||||
for(UUID uuid : packet.profileIds()) {
|
||||
Minecraft minecraft = Minecraft.getInstance();
|
||||
ClientPacketListener clientPacketListener = minecraft.getConnection();
|
||||
if(clientPacketListener instanceof IEDGClientPacketListener iedgClientPacketListener) {
|
||||
PlayerInfo playerinfo = iedgClientPacketListener.getNPCPlayerInfoMap().remove(uuid);
|
||||
if (playerinfo != null) {
|
||||
iedgClientPacketListener.getListedNPCPlayers().remove(playerinfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
context.setPacketHandled(true);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,242 @@
|
|||
/*
|
||||
* Copyright 2025-2026 R3944Realms
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.r3944realms.eroticdungeongame.core.network.toClient;
|
||||
|
||||
import com.eliotlash.mclib.math.functions.limit.Min;
|
||||
import com.mojang.authlib.GameProfile;
|
||||
import net.minecraft.Optionull;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.multiplayer.ClientPacketListener;
|
||||
import net.minecraft.client.multiplayer.PlayerInfo;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.chat.RemoteChatSession;
|
||||
import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.level.GameType;
|
||||
import net.minecraftforge.api.distmarker.Dist;
|
||||
import net.minecraftforge.api.distmarker.OnlyIn;
|
||||
import net.minecraftforge.network.NetworkEvent;
|
||||
import org.jetbrains.annotations.Contract;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import top.r3944realms.eroticdungeongame.EroticDungeon;
|
||||
import top.r3944realms.eroticdungeongame.content.entity.npc.NPCServerPlayer;
|
||||
import top.r3944realms.eroticdungeongame.util.IEDGClientPacketListener;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public final class NPCInfoUpdatePacket {
|
||||
private final EnumSet<Action> actions;
|
||||
private final List<Entry> entries;
|
||||
|
||||
public List<Entry> newEntries() {
|
||||
return this.actions.contains(Action.ADD_NPC) ? this.entries : List.of();
|
||||
}
|
||||
|
||||
public NPCInfoUpdatePacket(EnumSet<Action> actions, List<Entry> npcs) {
|
||||
this.actions = actions;
|
||||
this.entries = npcs;
|
||||
}
|
||||
|
||||
public NPCInfoUpdatePacket(Action action, NPCServerPlayer npc) {
|
||||
this(EnumSet.of(action), List.of(new Entry(npc)));
|
||||
}
|
||||
|
||||
public NPCInfoUpdatePacket(EnumSet<Action> actions, @NotNull Collection<NPCServerPlayer> players) {
|
||||
this(actions, players.stream().map(Entry::new).toList());
|
||||
}
|
||||
|
||||
public NPCInfoUpdatePacket(@NotNull FriendlyByteBuf buf) {
|
||||
this.actions = EnumSet.noneOf(Action.class);
|
||||
this.entries = buf.readList((read) -> {
|
||||
EntryBuilder builder = new EntryBuilder(read.readUUID());
|
||||
|
||||
for(Action action : this.actions) {
|
||||
action.reader.read(builder, read);
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
});
|
||||
}
|
||||
public void write(@NotNull FriendlyByteBuf buffer) {
|
||||
buffer.writeEnumSet(this.actions, Action.class);
|
||||
buffer.writeCollection(this.entries, (buf, entry) -> {
|
||||
buf.writeUUID(entry.profileId());
|
||||
|
||||
for(Action action : this.actions) {
|
||||
action.writer.write(buf, entry);
|
||||
}
|
||||
});
|
||||
}
|
||||
public static @NotNull NPCInfoUpdatePacket createNPCPlayerInitializing(Collection<NPCServerPlayer> players) {
|
||||
EnumSet<Action> enumset = EnumSet.of(Action.ADD_NPC, Action.INITIALIZE_CHAT, Action.UPDATE_GAME_MODE, Action.UPDATE_LISTED, Action.UPDATE_LATENCY, Action.UPDATE_DISPLAY_NAME);
|
||||
return new NPCInfoUpdatePacket(enumset, players);
|
||||
}
|
||||
|
||||
public static void handle(NPCInfoUpdatePacket packet, @NotNull Supplier<NetworkEvent.Context> contextSupplier) {
|
||||
NetworkEvent.Context context = contextSupplier.get();
|
||||
context.enqueueWork(() -> {
|
||||
Minecraft minecraft = Minecraft.getInstance();
|
||||
ClientPacketListener clientPacketListener = minecraft.getConnection();
|
||||
if (clientPacketListener instanceof IEDGClientPacketListener iedgClientPacketListener) {
|
||||
for (Entry entry : packet.newEntries()) {
|
||||
PlayerInfo npcInfo = new PlayerInfo(entry.profile, false);
|
||||
iedgClientPacketListener.getNPCPlayerInfoMap().putIfAbsent(entry.profileId(), npcInfo);
|
||||
}
|
||||
for (Entry entry : packet.entries) {
|
||||
PlayerInfo npcInfo = iedgClientPacketListener.getNPCPlayerInfoMap().get(entry.profileId);
|
||||
if (npcInfo == null) {
|
||||
EroticDungeon.getLogger().warn("Ignoring npc player info update for unknown npc player {}", entry.profileId());
|
||||
} else {
|
||||
for(Action action : packet.actions()) {
|
||||
applyPlayerInfoUpdate(action, entry, npcInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
context.setPacketHandled(true);
|
||||
}
|
||||
@OnlyIn(Dist.CLIENT)
|
||||
private static void applyPlayerInfoUpdate(@NotNull Action action, Entry entry, PlayerInfo playerInfo) {
|
||||
Minecraft minecraft = Minecraft.getInstance();
|
||||
ClientPacketListener clientPacketListener = minecraft.getConnection();
|
||||
if (clientPacketListener instanceof IEDGClientPacketListener iedgClientPacketListener) {
|
||||
switch (action) {
|
||||
case INITIALIZE_CHAT:
|
||||
ClientboundPlayerInfoUpdatePacket.Entry copyEntry = new ClientboundPlayerInfoUpdatePacket.Entry(
|
||||
entry.profileId, entry.profile, entry.listed, entry.latency, entry.gameMode, entry.displayName, entry.chatSession
|
||||
);
|
||||
clientPacketListener.initializeChatSession(copyEntry, playerInfo);
|
||||
break;
|
||||
case UPDATE_GAME_MODE:
|
||||
|
||||
if (playerInfo.getGameMode() != entry.gameMode() && minecraft.player != null && minecraft.player.getUUID().equals(entry.profileId())) {
|
||||
minecraft.player.onGameModeChanged(entry.gameMode());
|
||||
}
|
||||
|
||||
playerInfo.setGameMode(entry.gameMode());
|
||||
break;
|
||||
case UPDATE_LISTED:
|
||||
if (entry.listed()) {
|
||||
iedgClientPacketListener.getListedNPCPlayers().add(playerInfo);
|
||||
} else {
|
||||
iedgClientPacketListener.getListedNPCPlayers().remove(playerInfo);
|
||||
}
|
||||
break;
|
||||
case UPDATE_LATENCY:
|
||||
playerInfo.setLatency(entry.latency());
|
||||
break;
|
||||
case UPDATE_DISPLAY_NAME:
|
||||
playerInfo.setTabListDisplayName(entry.displayName());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
public EnumSet<Action> actions() {
|
||||
return actions;
|
||||
}
|
||||
|
||||
public List<Entry> npcs() {
|
||||
return entries;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj == this) return true;
|
||||
if (obj == null || obj.getClass() != this.getClass()) return false;
|
||||
var that = (NPCInfoUpdatePacket) obj;
|
||||
return Objects.equals(this.actions, that.actions) &&
|
||||
Objects.equals(this.entries, that.entries);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(actions, entries);
|
||||
}
|
||||
|
||||
@Contract(pure = true)
|
||||
@Override
|
||||
public @NotNull String toString() {
|
||||
return "NPCInfoUpdatePacket[" +
|
||||
"actions=" + actions + ", " +
|
||||
"npcs=" + entries + ']';
|
||||
}
|
||||
|
||||
|
||||
public enum Action {
|
||||
ADD_NPC((entryBuilder, buf) -> {
|
||||
GameProfile gameprofile = new GameProfile(entryBuilder.profileId, buf.readUtf(16));
|
||||
gameprofile.getProperties().putAll(buf.readGameProfileProperties());
|
||||
entryBuilder.profile = gameprofile;
|
||||
}, (buf, entry) -> {
|
||||
buf.writeUtf(entry.profile().getName(), 16);
|
||||
buf.writeGameProfileProperties(entry.profile().getProperties());
|
||||
}),
|
||||
INITIALIZE_CHAT((entryBuilder, buf) -> entryBuilder.chatSession = buf.readNullable(RemoteChatSession.Data::read), (buf, entry) -> buf.writeNullable(entry.chatSession, RemoteChatSession.Data::write)),
|
||||
UPDATE_GAME_MODE((entryBuilder, buf) -> entryBuilder.gameMode = GameType.byId(buf.readVarInt()), (buf, entry) -> buf.writeVarInt(entry.gameMode().getId())),
|
||||
UPDATE_LISTED((entryBuilder, buf) -> entryBuilder.listed = buf.readBoolean(), (buf, entry) -> buf.writeBoolean(entry.listed())),
|
||||
UPDATE_LATENCY((entryBuilder, buf) -> entryBuilder.latency = buf.readVarInt(), (buf, entry) -> buf.writeVarInt(entry.latency())),
|
||||
UPDATE_DISPLAY_NAME((entryBuilder, buf) -> entryBuilder.displayName = buf.readNullable(FriendlyByteBuf::readComponent), (buf, entry) -> buf.writeNullable(entry.displayName(), FriendlyByteBuf::writeComponent));
|
||||
final Reader reader;
|
||||
final Writer writer;
|
||||
|
||||
Action(Reader reader, Writer writer) {
|
||||
this.reader = reader;
|
||||
this.writer = writer;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public record Entry(UUID profileId, GameProfile profile, boolean listed, int latency, GameType gameMode,
|
||||
@Nullable Component displayName, @Nullable RemoteChatSession.Data chatSession) {
|
||||
Entry(@NotNull NPCServerPlayer npc) {
|
||||
this(npc.getUUID(), npc.getGameProfile(), true, npc.latency, npc.gameMode.getGameModeForPlayer(), npc.getTabListDisplayName(), Optionull.map(npc.getChatSession(), RemoteChatSession::asData));
|
||||
}
|
||||
}
|
||||
|
||||
public interface Reader {
|
||||
void read(EntryBuilder entryBuilder, FriendlyByteBuf buffer);
|
||||
}
|
||||
|
||||
public interface Writer {
|
||||
void write(FriendlyByteBuf buffer, Entry entry);
|
||||
}
|
||||
|
||||
public static class EntryBuilder {
|
||||
final UUID profileId;
|
||||
GameProfile profile;
|
||||
boolean listed;
|
||||
int latency;
|
||||
GameType gameMode = GameType.DEFAULT_MODE;
|
||||
@Nullable
|
||||
Component displayName;
|
||||
@Nullable
|
||||
RemoteChatSession.Data chatSession;
|
||||
|
||||
EntryBuilder(UUID profileId) {
|
||||
this.profileId = profileId;
|
||||
this.profile = new GameProfile(profileId, null);
|
||||
}
|
||||
|
||||
Entry build() {
|
||||
return new Entry(this.profileId, this.profile, this.listed, this.latency, this.gameMode, this.displayName, this.chatSession);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* Copyright 2025-2026 R3944Realms
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.r3944realms.eroticdungeongame.core.storage;
|
||||
|
||||
import com.mojang.datafixers.DataFixer;
|
||||
import com.mojang.logging.LogUtils;
|
||||
import net.minecraft.Util;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.NbtIo;
|
||||
import net.minecraft.nbt.NbtUtils;
|
||||
import net.minecraft.util.datafix.DataFixTypes;
|
||||
import net.minecraft.world.level.storage.LevelResource;
|
||||
import net.minecraft.world.level.storage.LevelStorageSource;
|
||||
import org.slf4j.Logger;
|
||||
import top.r3944realms.eroticdungeongame.content.entity.npc.NPCServerPlayer;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.File;
|
||||
|
||||
public class NPCDataStorage {
|
||||
private static final Logger LOGGER = LogUtils.getLogger();
|
||||
private final File npcDir;
|
||||
protected final DataFixer fixerUpper;
|
||||
public static LevelResource NPC_DATA_DIR = new LevelResource("npcdata");
|
||||
public NPCDataStorage(LevelStorageSource.LevelStorageAccess levelStorageAccess, DataFixer fixerUpper) {
|
||||
this.fixerUpper = fixerUpper;
|
||||
this.npcDir = levelStorageAccess.getLevelPath(NPC_DATA_DIR).toFile();
|
||||
this.npcDir.mkdirs();
|
||||
}
|
||||
public void save(NPCServerPlayer player) {
|
||||
try {
|
||||
CompoundTag compoundtag = player.saveWithoutId(new CompoundTag());
|
||||
File file1 = File.createTempFile(player.getStringUUID() + "-", ".dat", this.npcDir);
|
||||
NbtIo.writeCompressed(compoundtag, file1);
|
||||
File file2 = new File(this.npcDir, player.getStringUUID() + ".dat");
|
||||
File file3 = new File(this.npcDir, player.getStringUUID() + ".dat_old");
|
||||
Util.safeReplaceFile(file2, file1, file3);
|
||||
net.minecraftforge.event.ForgeEventFactory.firePlayerSavingEvent(player, npcDir, player.getStringUUID());
|
||||
} catch (Exception exception) {
|
||||
LOGGER.warn("Failed to save player data for {}", player.getName().getString());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public CompoundTag load(NPCServerPlayer npcServerPlayer) {
|
||||
CompoundTag compoundtag = null;
|
||||
|
||||
try {
|
||||
File file1 = new File(this.npcDir, npcServerPlayer.getStringUUID() + ".dat");
|
||||
if (file1.exists() && file1.isFile()) {
|
||||
compoundtag = NbtIo.readCompressed(file1);
|
||||
}
|
||||
} catch (Exception exception) {
|
||||
LOGGER.warn("Failed to load player data for {}", npcServerPlayer.getName().getString());
|
||||
}
|
||||
|
||||
if (compoundtag != null) {
|
||||
int i = NbtUtils.getDataVersion(compoundtag, -1);
|
||||
npcServerPlayer.load(DataFixTypes.PLAYER.updateToCurrentVersion(this.fixerUpper, compoundtag, i));
|
||||
}
|
||||
// net.minecraftforge.event.ForgeEventFactory.firePlayerLoadingEvent(npcServerPlayer, npcDir, npcServerPlayer.getStringUUID());
|
||||
|
||||
return compoundtag;
|
||||
}
|
||||
|
||||
public String[] getSeenNPCs() {
|
||||
String[] astring = this.npcDir.list();
|
||||
if (astring == null) {
|
||||
astring = new String[0];
|
||||
}
|
||||
|
||||
for(int i = 0; i < astring.length; ++i) {
|
||||
if (astring[i].endsWith(".dat")) {
|
||||
astring[i] = astring[i].substring(0, astring[i].length() - 4);
|
||||
}
|
||||
}
|
||||
|
||||
return astring;
|
||||
}
|
||||
|
||||
public File getNPCDataFolder() {
|
||||
return npcDir;
|
||||
}
|
||||
}
|
||||
|
|
@ -49,7 +49,7 @@ public class MixinCamera {
|
|||
target = "Lnet/minecraft/world/level/BlockGetter;clip(Lnet/minecraft/world/level/ClipContext;)Lnet/minecraft/world/phys/BlockHitResult;"
|
||||
)
|
||||
)
|
||||
private BlockHitResult getMaxZoomFix(BlockGetter instance, ClipContext context, Operation<BlockHitResult> original) {
|
||||
private BlockHitResult edg$getMaxZoomFix(BlockGetter instance, ClipContext context, Operation<BlockHitResult> original) {
|
||||
return Services.WORK_SPACE.tryToDoIfInDeviceAndRet(
|
||||
Minecraft.getInstance().player,
|
||||
data -> {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright 2025-2026 R3944Realms
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.r3944realms.eroticdungeongame.mixin.minecraft;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import net.minecraft.network.Connection;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.gen.Accessor;
|
||||
import top.r3944realms.eroticdungeongame.util.IClientConnection;
|
||||
|
||||
@Mixin(Connection.class)
|
||||
public abstract class MixinClientConnect implements IClientConnection {
|
||||
@Accessor
|
||||
@Override
|
||||
public abstract void setChannel(Channel var1);
|
||||
}
|
||||
|
|
@ -16,9 +16,16 @@
|
|||
|
||||
package top.r3944realms.eroticdungeongame.mixin.minecraft;
|
||||
|
||||
import com.google.common.collect.Maps;
|
||||
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
|
||||
import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.multiplayer.ClientPacketListener;
|
||||
import net.minecraft.client.multiplayer.PlayerInfo;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.protocol.PacketUtils;
|
||||
import net.minecraft.network.protocol.game.ClientboundAddPlayerPacket;
|
||||
import net.minecraft.network.protocol.game.ClientboundSetPassengersPacket;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import org.spongepowered.asm.mixin.Final;
|
||||
|
|
@ -32,26 +39,62 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
|||
import org.spongepowered.asm.mixin.injection.callback.LocalCapture;
|
||||
import top.r3944realms.eroticdungeongame.client.EDGKeyBindings;
|
||||
import top.r3944realms.eroticdungeongame.content.entity.SeatEntity;
|
||||
import top.r3944realms.eroticdungeongame.util.IEDGClientPacketListener;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
@Mixin(ClientPacketListener.class)
|
||||
public class MixinClientPacketListener {
|
||||
public abstract class MixinClientPacketListener implements IEDGClientPacketListener {
|
||||
@Unique
|
||||
private final Map<UUID, PlayerInfo> edg$npcPlayerInfoMap = Maps.newHashMap();
|
||||
@Unique
|
||||
private final Set<PlayerInfo> edg$listedNPCPlayers = new ReferenceOpenHashSet<>();
|
||||
@Shadow
|
||||
@Final
|
||||
private Minecraft minecraft;
|
||||
|
||||
@Shadow @Nullable public abstract PlayerInfo getPlayerInfo(String name);
|
||||
|
||||
@Shadow @Nullable public abstract PlayerInfo getPlayerInfo(UUID uniqueId);
|
||||
|
||||
@Shadow @Final private Set<PlayerInfo> listedPlayers;
|
||||
@Unique
|
||||
private Entity edg$cachedMount = null;
|
||||
|
||||
@Inject(method = "handleSetEntityPassengersPacket", locals = LocalCapture.CAPTURE_FAILEXCEPTION,
|
||||
at = @At(value = "INVOKE_ASSIGN", ordinal = 0, shift = At.Shift.AFTER, target = "Lnet/minecraft/client/multiplayer/ClientLevel;getEntity(I)Lnet/minecraft/world/entity/Entity;"))
|
||||
private void cacheMountedEntity(ClientboundSetPassengersPacket packet, CallbackInfo ci, Entity mounted) {
|
||||
private void edg$cacheMountedEntity(ClientboundSetPassengersPacket packet, CallbackInfo ci, Entity mounted) {
|
||||
edg$cachedMount = mounted;
|
||||
}
|
||||
|
||||
@ModifyVariable(method = "handleSetEntityPassengersPacket", index = 9,
|
||||
at = @At(value = "INVOKE_ASSIGN", ordinal = 0, shift = At.Shift.AFTER, target = "Lnet/minecraft/network/chat/Component;translatable(Ljava/lang/String;[Ljava/lang/Object;)Lnet/minecraft/network/chat/MutableComponent;"))
|
||||
private Component modifyMountMessage(Component old) {
|
||||
private Component edg$modifyMountMessage(Component old) {
|
||||
if(edg$cachedMount != null && edg$cachedMount instanceof SeatEntity) {
|
||||
return Component.translatable("mount.eroticdungeongame.seat.onboard", EDGKeyBindings.KEY_QUIT.getTranslatedKeyMessage());
|
||||
} else return old;
|
||||
}
|
||||
@WrapMethod(
|
||||
method = "handleAddPlayer"
|
||||
)
|
||||
private void edg$handleAddPlayer(ClientboundAddPlayerPacket packet, Operation<Void> original){
|
||||
PacketUtils.ensureRunningOnSameThread(packet, ClientPacketListener.class.cast(this), this.minecraft);
|
||||
PlayerInfo playerinfo = this.getPlayerInfo(packet.getPlayerId());
|
||||
if (playerinfo == null) {
|
||||
|
||||
} else original.call(packet);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<UUID, PlayerInfo> getNPCPlayerInfoMap() {
|
||||
return edg$npcPlayerInfoMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<PlayerInfo> getListedNPCPlayers() {
|
||||
return listedPlayers;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ package top.r3944realms.eroticdungeongame.mixin.minecraft;
|
|||
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
|
|
@ -28,6 +29,7 @@ import org.spongepowered.asm.mixin.Shadow;
|
|||
import top.r3944realms.eroticdungeongame.api.workspace.Services;
|
||||
import top.r3944realms.eroticdungeongame.content.block.blockentity.BaseSeatBlockEntity;
|
||||
import top.r3944realms.eroticdungeongame.content.entity.SeatEntity;
|
||||
import top.r3944realms.eroticdungeongame.content.entity.npc.NPCServerPlayer;
|
||||
import top.r3944realms.eroticdungeongame.core.service.SeatService;
|
||||
import top.r3944realms.eroticdungeongame.util.BoundingBoxCalculator;
|
||||
import top.r3944realms.eroticdungeongame.util.IEDGEntity;
|
||||
|
|
@ -51,10 +53,12 @@ public abstract class MixinEntity implements IEDGEntity {
|
|||
|
||||
@Shadow public abstract void playerTouch(Player player);
|
||||
|
||||
@Shadow @Nullable public abstract LivingEntity getControllingPassenger();
|
||||
|
||||
@WrapMethod(
|
||||
method = "getBoundingBox"
|
||||
)
|
||||
private AABB redefinedBoundingBox(@NotNull Operation<AABB> original){
|
||||
private AABB edg$redefinedBoundingBox(@NotNull Operation<AABB> original){
|
||||
Entity self = Entity.class.cast(this);
|
||||
return Services.WORK_SPACE.tryToDoIfInDeviceAndRet(self, data-> {
|
||||
AABB playerBoundingBox = data.getPlayerBoundingBox();
|
||||
|
|
@ -67,7 +71,7 @@ public abstract class MixinEntity implements IEDGEntity {
|
|||
@WrapMethod(
|
||||
method = "startRiding(Lnet/minecraft/world/entity/Entity;Z)Z"
|
||||
)
|
||||
private boolean redefineStartRiding(Entity vehicle, boolean force, Operation<Boolean> original) {
|
||||
private boolean edg$redefineStartRiding(Entity vehicle, boolean force, Operation<Boolean> original) {
|
||||
Entity entity = Entity.class.cast(this);
|
||||
return Services.WORK_SPACE.tryToDoIfInDeviceAndRet(entity,
|
||||
data -> {
|
||||
|
|
@ -85,7 +89,7 @@ public abstract class MixinEntity implements IEDGEntity {
|
|||
@WrapMethod(
|
||||
method = "stopRiding"
|
||||
)
|
||||
private void redefineStopRiding(Operation<Void> original) {
|
||||
private void edg$redefineStopRiding(Operation<Void> original) {
|
||||
Entity entity = Entity.class.cast(this);
|
||||
Services.WORK_SPACE.tryToDoIfInDevice(entity,
|
||||
data -> {
|
||||
|
|
@ -101,6 +105,15 @@ public abstract class MixinEntity implements IEDGEntity {
|
|||
original::call
|
||||
);
|
||||
}
|
||||
@WrapMethod(
|
||||
method = {"isControlledByLocalInstance"}
|
||||
)
|
||||
private boolean isFakePlayer(Operation<Boolean> original) {
|
||||
if (this.getControllingPassenger() instanceof NPCServerPlayer) {
|
||||
return !this.level.isClientSide;
|
||||
}
|
||||
return original.call();
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("AddedMixinMembersNamePattern")
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import top.r3944realms.eroticdungeongame.api.workspace.Services;
|
|||
@Mixin(EntityRenderer.class)
|
||||
public abstract class MixinEntityRenderer<T extends Entity> {
|
||||
@WrapMethod(method = "getSkyLightLevel")
|
||||
protected int getSkyLightLevel(T entity, BlockPos pos, Operation<Integer> original) {
|
||||
protected int edg$getSkyLightLevel(T entity, BlockPos pos, Operation<Integer> original) {
|
||||
return Services.WORK_SPACE.tryToDoIfInDeviceAndRet(
|
||||
entity,
|
||||
data -> {
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ public class MixinGameRender {
|
|||
@WrapMethod(
|
||||
method = "renderItemInHand"
|
||||
)
|
||||
private void warpRenderHand(PoseStack poseStack, Camera activeRenderInfo, float partialTicks, Operation<Void> original) {
|
||||
private void edg$warpRenderHand(PoseStack poseStack, Camera activeRenderInfo, float partialTicks, Operation<Void> original) {
|
||||
Services.WORK_SPACE.tryToDoIfInDevice(minecraft.player, data -> {}, () -> original.call(poseStack, activeRenderInfo, partialTicks));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ public abstract class MixinItemInHandRenderer {
|
|||
@WrapMethod(
|
||||
method = "renderItem"
|
||||
)
|
||||
private void removeItemInHandRender(
|
||||
private void edg$removeItemInHandRender(
|
||||
LivingEntity entity, ItemStack itemStack, ItemDisplayContext displayContext, boolean leftHand, PoseStack poseStack, MultiBufferSource buffer, int seed, Operation<Void> original) {
|
||||
Services.WORK_SPACE.tryToDoIfInDevice(entity, data -> {
|
||||
if(data.getDeviceMainBlockPos() != null && !(entity.level().getBlockState(data.getDeviceMainBlockPos()).getBlock() instanceof DisplayRackBlock)) {
|
||||
|
|
|
|||
|
|
@ -46,14 +46,14 @@ public abstract class MixinLivingEntity extends Entity {
|
|||
target= "Lnet/minecraft/world/entity/LivingEntity;isInWall()Z"
|
||||
)
|
||||
)
|
||||
private boolean inWall$Warp(LivingEntity instance, Operation<Boolean> original) {
|
||||
private boolean edg$inWall$Warp(LivingEntity instance, Operation<Boolean> original) {
|
||||
return Services.WORK_SPACE.tryToDoIfInDeviceAndRet(instance, data -> false, () -> original.call(instance));
|
||||
}
|
||||
|
||||
@WrapMethod(
|
||||
method = "stopRiding"
|
||||
)
|
||||
private void redefineStopRiding(Operation<Void> original) {
|
||||
private void edg$redefineStopRiding(Operation<Void> original) {
|
||||
LivingEntity livingEntity = LivingEntity.class.cast(this);
|
||||
Services.WORK_SPACE.tryToDoIfInDevice(livingEntity,
|
||||
data -> {
|
||||
|
|
|
|||
|
|
@ -35,21 +35,21 @@ public class MixinMinecraft {
|
|||
@WrapMethod(
|
||||
method = "startAttack"
|
||||
)
|
||||
private boolean forbiddenAttack(Operation<Boolean> original){
|
||||
private boolean edg$forbiddenAttack(Operation<Boolean> original){
|
||||
return Services.WORK_SPACE.tryToDoIfInDeviceAndRet(player, data -> false, original::call);
|
||||
}
|
||||
|
||||
@WrapMethod(
|
||||
method = "startUseItem"
|
||||
)
|
||||
private void forbiddenUseItem(Operation<Void> original){
|
||||
private void edg$forbiddenUseItem(Operation<Void> original){
|
||||
Services.WORK_SPACE.tryToDoIfInDevice(player, data -> {}, original::call);
|
||||
}
|
||||
|
||||
@WrapMethod(
|
||||
method = "continueAttack"
|
||||
)
|
||||
private void forbiddenContinueAttack(boolean leftClick, Operation<Void> original){
|
||||
private void edg$forbiddenContinueAttack(boolean leftClick, Operation<Void> original){
|
||||
Services.WORK_SPACE.tryToDoIfInDeviceAndRet(player, data -> false, () -> original.call(leftClick));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* Copyright 2025-2026 R3944Realms
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.r3944realms.eroticdungeongame.mixin.minecraft;
|
||||
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.players.PlayerList;
|
||||
import net.minecraft.world.level.storage.LevelStorageSource;
|
||||
import net.minecraft.world.level.storage.WorldData;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import top.r3944realms.eroticdungeongame.content.entity.npc.NPCPlayerList;
|
||||
import top.r3944realms.eroticdungeongame.util.IEDGMinecraftServer;
|
||||
|
||||
@Mixin(MinecraftServer.class)
|
||||
public class MixinMinecraftServer implements IEDGMinecraftServer {
|
||||
|
||||
@Unique
|
||||
private NPCPlayerList edg$npcPlayerList;
|
||||
|
||||
@SuppressWarnings("AddedMixinMembersNamePattern")
|
||||
@Override
|
||||
public NPCPlayerList getNPCPlayerList() {
|
||||
return edg$npcPlayerList;
|
||||
}
|
||||
|
||||
@SuppressWarnings("AddedMixinMembersNamePattern")
|
||||
@Override
|
||||
public void setNPCPlayerList(NPCPlayerList playerList) {
|
||||
this.edg$npcPlayerList = playerList;
|
||||
}
|
||||
@WrapOperation(
|
||||
method = "saveEverything",
|
||||
at = @At(
|
||||
value = "INVOKE",
|
||||
target = "Lnet/minecraft/server/players/PlayerList;saveAll()V"
|
||||
)
|
||||
)
|
||||
void edg$saveEverything$saveAll(PlayerList instance, @NotNull Operation<Void> original) {
|
||||
original.call(instance);
|
||||
edg$npcPlayerList.saveAll();
|
||||
}
|
||||
@WrapOperation(
|
||||
method = "stopServer",
|
||||
at = @At(
|
||||
value = "INVOKE",
|
||||
target = "Lnet/minecraft/server/players/PlayerList;saveAll()V"
|
||||
)
|
||||
)
|
||||
void edg$stopServer$saveAll(PlayerList instance, @NotNull Operation<Void> original) {
|
||||
original.call(instance);
|
||||
edg$npcPlayerList.saveAll();
|
||||
edg$npcPlayerList.removeAll();
|
||||
}
|
||||
|
||||
@WrapOperation(
|
||||
method = "lambda$reloadResources$26",
|
||||
at = @At(
|
||||
value = "INVOKE",
|
||||
target = "Lnet/minecraft/server/players/PlayerList;reloadResources()V"
|
||||
)
|
||||
)
|
||||
void edg$reloadResources$saveAll(PlayerList instance, @NotNull Operation<Void> original) {
|
||||
original.call(instance);
|
||||
edg$npcPlayerList.saveAll();
|
||||
edg$npcPlayerList.removeAll();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -18,28 +18,59 @@ package top.r3944realms.eroticdungeongame.mixin.minecraft;
|
|||
|
||||
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
|
||||
import net.minecraft.world.entity.EntityType;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.level.Level;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Redirect;
|
||||
import top.r3944realms.eroticdungeongame.api.workspace.Services;
|
||||
import top.r3944realms.eroticdungeongame.content.entity.npc.INPCPlayer;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@Mixin(Player.class)
|
||||
public abstract class MixinPlayer extends LivingEntity {
|
||||
protected MixinPlayer(EntityType<? extends LivingEntity> entityType, Level level) {
|
||||
super(entityType, level);
|
||||
}
|
||||
|
||||
@Redirect(
|
||||
method = "<init>",
|
||||
at = @At(
|
||||
value = "INVOKE",
|
||||
target = "Lnet/minecraft/world/entity/player/Player;setUUID(Ljava/util/UUID;)V"
|
||||
)
|
||||
)
|
||||
private void edg$Player$setUUID(Player instance, UUID uuid) {
|
||||
if (!(instance instanceof INPCPlayer)) {
|
||||
instance.setUUID(uuid);
|
||||
}
|
||||
}
|
||||
@WrapMethod(method = "shouldShowName")
|
||||
public boolean shouldShowName(Operation<Boolean> original) {
|
||||
public boolean edg$shouldShowName(@NotNull Operation<Boolean> original) {
|
||||
Player player = Player.class.cast(this);
|
||||
return Services.WORK_SPACE.tryToDoIfInDeviceAndRet(player, data -> false, original::call);
|
||||
}
|
||||
|
||||
@WrapMethod(method = "wantsToStopRiding")
|
||||
private boolean wantToStopRide(Operation<Boolean> original) {
|
||||
private boolean edg$wantToStopRide(@NotNull Operation<Boolean> original) {
|
||||
Player player = Player.class.cast(this);
|
||||
return Services.WORK_SPACE.tryToDoIfInDeviceAndRet(player, data -> false, original::call);
|
||||
}
|
||||
@WrapOperation(
|
||||
method = "readAdditionalSaveData",
|
||||
at = @At(
|
||||
value = "INVOKE",
|
||||
target = "Lnet/minecraft/world/entity/player/Player;setUUID(Ljava/util/UUID;)V"
|
||||
)
|
||||
)
|
||||
private void edg$readAdditionalSaveData$setUUID(Player instance, UUID uuid, Operation<Void> original) {
|
||||
if (!(instance instanceof INPCPlayer)) {
|
||||
original.call(instance, uuid);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ public class MixinPlayerRenderer {
|
|||
@WrapMethod(
|
||||
method = "renderNameTag(Lnet/minecraft/client/player/AbstractClientPlayer;Lnet/minecraft/network/chat/Component;Lcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/MultiBufferSource;I)V"
|
||||
)
|
||||
private void renderNameTag(AbstractClientPlayer entity, Component displayName, PoseStack poseStack, MultiBufferSource buffer, int packedLight, Operation<Void> original) {
|
||||
private void edg$renderNameTag(AbstractClientPlayer entity, Component displayName, PoseStack poseStack, MultiBufferSource buffer, int packedLight, Operation<Void> original) {
|
||||
Services.WORK_SPACE.tryToDoIfInDevice(entity, data -> {}, () -> original.call(entity, displayName, poseStack, buffer, packedLight));
|
||||
}
|
||||
|
||||
|
|
@ -50,7 +50,7 @@ public class MixinPlayerRenderer {
|
|||
)
|
||||
|
||||
)
|
||||
private <T extends LivingEntity> void renderFix(PlayerRenderer instance, T entity, float entityYaw, float partialTicks, PoseStack poseStack, MultiBufferSource buffer, int packedLight, Operation<Void> original) {
|
||||
private <T extends LivingEntity> void edg$renderFix(PlayerRenderer instance, T entity, float entityYaw, float partialTicks, PoseStack poseStack, MultiBufferSource buffer, int packedLight, Operation<Void> original) {
|
||||
Services.WORK_SPACE.tryToDoIfInDevice(entity, data -> {
|
||||
Minecraft minecraft = Minecraft.getInstance();
|
||||
if(!(Objects.equals(minecraft.player, entity) && minecraft.options.getCameraType().isFirstPerson() &&
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ public abstract class MixinServerPlayer extends Player implements IEDGEntity {
|
|||
@WrapMethod(
|
||||
method = "stopRiding"
|
||||
)
|
||||
private void redefineStopRiding(Operation<Void> original) {
|
||||
private void edg$redefineStopRiding(Operation<Void> original) {
|
||||
ServerPlayer player = ServerPlayer.class.cast(this);
|
||||
Services.WORK_SPACE.tryToDoIfInDevice(player,
|
||||
data -> {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright 2025-2026 R3944Realms
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.r3944realms.eroticdungeongame.util;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
|
||||
public interface IClientConnection {
|
||||
void setChannel(Channel var1);
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Copyright 2025-2026 R3944Realms
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.r3944realms.eroticdungeongame.util;
|
||||
|
||||
import net.minecraft.client.multiplayer.PlayerInfo;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface IEDGClientPacketListener {
|
||||
Map<UUID, PlayerInfo> getNPCPlayerInfoMap();
|
||||
Set<PlayerInfo> getListedNPCPlayers();
|
||||
default Collection<PlayerInfo> getOnlineNPCPlayers() {
|
||||
return getNPCPlayerInfoMap().values();
|
||||
}
|
||||
default Collection<UUID> getOnlineNPCPlayerIds() {
|
||||
return getNPCPlayerInfoMap().keySet();
|
||||
}
|
||||
@Nullable
|
||||
default PlayerInfo getNPCPlayerInfo(UUID uniqueId) {
|
||||
return getNPCPlayerInfoMap().get(uniqueId);
|
||||
}
|
||||
@Nullable
|
||||
default PlayerInfo getNPCPlayerInfo(String name) {
|
||||
for(PlayerInfo playerinfo : getNPCPlayerInfoMap().values()) {
|
||||
if (playerinfo.getProfile().getName().equals(name)) {
|
||||
return playerinfo;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Copyright 2025-2026 R3944Realms
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.r3944realms.eroticdungeongame.util;
|
||||
|
||||
import top.r3944realms.eroticdungeongame.content.entity.npc.NPCPlayerList;
|
||||
|
||||
public interface IEDGMinecraftServer {
|
||||
NPCPlayerList getNPCPlayerList();
|
||||
void setNPCPlayerList(NPCPlayerList playerList);
|
||||
}
|
||||
|
|
@ -7,9 +7,12 @@
|
|||
"refmap": "eroticdungeongame.refmap.json",
|
||||
"mixins": [
|
||||
"bendylib.MixinBendableCuboidBuilder",
|
||||
"minecraft.MixinClientConnect",
|
||||
"minecraft.MixinEntity",
|
||||
"minecraft.MixinLivingEntity",
|
||||
"minecraft.MixinMinecraftServer",
|
||||
"minecraft.MixinPlayer",
|
||||
"minecraft.MixinServerLoginPacketListenerImpl",
|
||||
"minecraft.MixinServerPlayer"
|
||||
],
|
||||
"client": [
|
||||
|
|
|
|||
|
|
@ -40,4 +40,9 @@ protected net.minecraft.client.gui.screens.recipebook.RecipeButton f_100467_ # a
|
|||
protected net.minecraft.client.gui.screens.recipebook.RecipeButton f_100468_ # currentIndex
|
||||
protected net.minecraft.client.gui.screens.recipebook.RecipeButton m_100490_()Ljava/util/List; # getOrderedRecipes
|
||||
public net.minecraft.world.entity.Entity f_19816_ # eyeHeight
|
||||
public net.minecraft.client.Camera m_90581_(Lnet/minecraft/world/phys/Vec3;)V # setPosition
|
||||
public net.minecraft.client.Camera m_90581_(Lnet/minecraft/world/phys/Vec3;)V # setPosition
|
||||
public net.minecraft.client.multiplayer.ClientPacketListener m_245842_(Lnet/minecraft/network/protocol/game/ClientboundPlayerInfoUpdatePacket$Entry;Lnet/minecraft/client/multiplayer/PlayerInfo;)V # initializeChatSession
|
||||
public net.minecraft.client.multiplayer.ClientPacketListener f_244156_ # listedPlayers
|
||||
public net.minecraft.client.multiplayer.PlayerInfo m_105317_(Lnet/minecraft/world/level/GameType;)V # setGameMode
|
||||
public net.minecraft.client.multiplayer.PlayerInfo m_105313_(I)V # setLatency
|
||||
public net.minecraft.client.multiplayer.ClientPacketListener f_104892_ # playerInfoMap
|
||||
Loading…
Reference in New Issue
Block a user