|
1 | 1 | package com.pyritone.bridge; |
2 | 2 |
|
3 | | -import com.google.gson.JsonElement; |
4 | 3 | import com.google.gson.JsonArray; |
| 4 | +import com.google.gson.JsonElement; |
| 5 | +import com.google.gson.JsonNull; |
5 | 6 | import com.google.gson.JsonObject; |
6 | 7 | import com.pyritone.bridge.command.PyritoneCommand; |
7 | 8 | import com.pyritone.bridge.config.BridgeConfig; |
|
11 | 12 | import com.pyritone.bridge.net.WebSocketBridgeServer; |
12 | 13 | import com.pyritone.bridge.runtime.BaritoneGateway; |
13 | 14 | import com.pyritone.bridge.runtime.EntityTypeSelector; |
| 15 | +import com.pyritone.bridge.runtime.PlayerLifecycleTracker; |
14 | 16 | import com.pyritone.bridge.runtime.StatusSubscriptionRegistry; |
15 | 17 | import com.pyritone.bridge.runtime.TaskRegistry; |
16 | 18 | import com.pyritone.bridge.runtime.TaskLifecycleResolver; |
|
19 | 21 | import com.pyritone.bridge.runtime.TypedApiException; |
20 | 22 | import com.pyritone.bridge.runtime.TypedApiService; |
21 | 23 | import com.pyritone.bridge.runtime.WatchPatternRegistry; |
| 24 | +import com.mojang.authlib.GameProfile; |
22 | 25 | import net.fabricmc.api.ClientModInitializer; |
23 | 26 | import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; |
| 27 | +import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents; |
24 | 28 | import net.fabricmc.fabric.api.client.message.v1.ClientSendMessageEvents; |
25 | 29 | import net.fabricmc.loader.api.FabricLoader; |
26 | 30 | import net.minecraft.client.MinecraftClient; |
| 31 | +import net.minecraft.client.network.PlayerListEntry; |
27 | 32 | import net.minecraft.entity.Entity; |
| 33 | +import net.minecraft.entity.player.PlayerEntity; |
28 | 34 | import net.minecraft.registry.Registries; |
29 | 35 | import net.minecraft.text.MutableText; |
30 | 36 | import net.minecraft.text.Text; |
|
41 | 47 | import java.time.Instant; |
42 | 48 | import java.util.ArrayList; |
43 | 49 | import java.util.Comparator; |
| 50 | +import java.util.HashMap; |
44 | 51 | import java.util.List; |
45 | 52 | import java.util.Locale; |
| 53 | +import java.util.Map; |
46 | 54 | import java.util.Optional; |
47 | 55 | import java.util.Set; |
48 | 56 | import java.util.concurrent.CompletionException; |
@@ -73,6 +81,10 @@ public final class PyritoneBridgeClientMod implements ClientModInitializer { |
73 | 81 | private final WatchPatternRegistry watchPatternRegistry = new WatchPatternRegistry(); |
74 | 82 | private final StatusSubscriptionRegistry statusSubscriptionRegistry = new StatusSubscriptionRegistry(); |
75 | 83 | private final TypedApiService typedApiService = new TypedApiService(PyritoneBridgeClientMod.class.getClassLoader()); |
| 84 | + private final PlayerLifecycleTracker playerLifecycleTracker = new PlayerLifecycleTracker(); |
| 85 | + private boolean playerLifecycleInWorld; |
| 86 | + private boolean playerLifecycleSelfJoinEmitted; |
| 87 | + private PlayerLifecycleTracker.PlayerSnapshot lastKnownSelfPlayer; |
76 | 88 |
|
77 | 89 | private final Object pauseStateLock = new Object(); |
78 | 90 | private volatile boolean operatorPauseActive; |
@@ -107,9 +119,14 @@ public void onInitializeClient() { |
107 | 119 | ); |
108 | 120 | baritoneGateway.tickApplyPyritoneChatBranding(); |
109 | 121 | tickPauseState(client); |
| 122 | + tickPlayerLifecycleEvents(client); |
110 | 123 | tickTaskLifecycle(); |
111 | 124 | tickStatusStreams(); |
112 | 125 | }); |
| 126 | + ClientReceiveMessageEvents.CHAT.register( |
| 127 | + (message, signedMessage, sender, params, receptionTimestamp) -> onIncomingChatMessage(message, sender) |
| 128 | + ); |
| 129 | + ClientReceiveMessageEvents.GAME.register(this::onIncomingSystemMessage); |
113 | 130 | ClientSendMessageEvents.ALLOW_CHAT.register(this::handleOutgoingChat); |
114 | 131 | ClientSendMessageEvents.ALLOW_COMMAND.register(this::handleOutgoingCommand); |
115 | 132 |
|
@@ -211,6 +228,10 @@ private void startBridgeServer() { |
211 | 228 |
|
212 | 229 | private void shutdownBridgeServer() { |
213 | 230 | clearPauseStateForShutdown(); |
| 231 | + playerLifecycleTracker.reset(); |
| 232 | + playerLifecycleInWorld = false; |
| 233 | + playerLifecycleSelfJoinEmitted = false; |
| 234 | + lastKnownSelfPlayer = null; |
214 | 235 | statusSubscriptionRegistry.clear(); |
215 | 236 | typedApiService.clear(); |
216 | 237 | if (this.server != null) { |
@@ -414,6 +435,8 @@ private JsonObject buildStatusPayload(WebSocketBridgeServer.ClientSession sessio |
414 | 435 | result.addProperty("in_world", baritoneGateway.isInWorld()); |
415 | 436 | result.add("active_task", taskRegistry.activeAsJson()); |
416 | 437 | result.add("watch_patterns", watchPatternRegistry.toJsonArray()); |
| 438 | + JsonObject player = currentPlayerPayload(MinecraftClient.getInstance()); |
| 439 | + result.add("player", player != null ? player : JsonNull.INSTANCE); |
417 | 440 | return result; |
418 | 441 | } |
419 | 442 |
|
@@ -648,6 +671,86 @@ private void tickPauseState(MinecraftClient client) { |
648 | 671 | setGamePauseActive(paused); |
649 | 672 | } |
650 | 673 |
|
| 674 | + private void tickPlayerLifecycleEvents(MinecraftClient client) { |
| 675 | + if (client == null || client.world == null || client.getNetworkHandler() == null) { |
| 676 | + if (playerLifecycleInWorld && lastKnownSelfPlayer != null) { |
| 677 | + emitPlayerLifecycleEvent("minecraft.player_leave", lastKnownSelfPlayer); |
| 678 | + } |
| 679 | + playerLifecycleTracker.reset(); |
| 680 | + playerLifecycleInWorld = false; |
| 681 | + playerLifecycleSelfJoinEmitted = false; |
| 682 | + lastKnownSelfPlayer = null; |
| 683 | + return; |
| 684 | + } |
| 685 | + |
| 686 | + if (!playerLifecycleInWorld) { |
| 687 | + playerLifecycleInWorld = true; |
| 688 | + playerLifecycleSelfJoinEmitted = false; |
| 689 | + } |
| 690 | + |
| 691 | + Map<String, Boolean> aliveByUuid = new HashMap<>(); |
| 692 | + String localPlayerUuid = null; |
| 693 | + if (client.player != null) { |
| 694 | + localPlayerUuid = client.player.getUuidAsString(); |
| 695 | + } |
| 696 | + |
| 697 | + for (PlayerEntity player : client.world.getPlayers()) { |
| 698 | + if (player == null) { |
| 699 | + continue; |
| 700 | + } |
| 701 | + String uuid = player.getUuidAsString(); |
| 702 | + if (uuid == null || uuid.isBlank()) { |
| 703 | + continue; |
| 704 | + } |
| 705 | + aliveByUuid.put(uuid, player.isAlive()); |
| 706 | + } |
| 707 | + |
| 708 | + List<PlayerLifecycleTracker.PlayerSnapshot> players = new ArrayList<>(); |
| 709 | + PlayerLifecycleTracker.PlayerSnapshot selfSnapshot = null; |
| 710 | + for (PlayerListEntry entry : client.getNetworkHandler().getPlayerList()) { |
| 711 | + if (entry == null || entry.getProfile() == null || entry.getProfile().getId() == null) { |
| 712 | + continue; |
| 713 | + } |
| 714 | + String uuid = entry.getProfile().getId().toString(); |
| 715 | + String name = entry.getProfile().getName(); |
| 716 | + if (name == null || name.isBlank()) { |
| 717 | + name = "unknown"; |
| 718 | + } |
| 719 | + boolean self = localPlayerUuid != null && localPlayerUuid.equals(uuid); |
| 720 | + Boolean aliveValue = aliveByUuid.get(uuid); |
| 721 | + boolean aliveKnown = aliveValue != null; |
| 722 | + boolean alive = aliveKnown && aliveValue; |
| 723 | + PlayerLifecycleTracker.PlayerSnapshot snapshot = |
| 724 | + new PlayerLifecycleTracker.PlayerSnapshot(uuid, name, alive, self, aliveKnown); |
| 725 | + players.add(snapshot); |
| 726 | + if (self) { |
| 727 | + selfSnapshot = snapshot; |
| 728 | + } |
| 729 | + } |
| 730 | + |
| 731 | + List<PlayerLifecycleTracker.PlayerEvent> events = playerLifecycleTracker.update(players); |
| 732 | + for (PlayerLifecycleTracker.PlayerEvent event : events) { |
| 733 | + String eventName = switch (event.type()) { |
| 734 | + case JOIN -> "minecraft.player_join"; |
| 735 | + case LEAVE -> "minecraft.player_leave"; |
| 736 | + case DEATH -> "minecraft.player_death"; |
| 737 | + case RESPAWN -> "minecraft.player_respawn"; |
| 738 | + }; |
| 739 | + emitPlayerLifecycleEvent(eventName, event.player()); |
| 740 | + if (event.type() == PlayerLifecycleTracker.PlayerEventType.JOIN |
| 741 | + && event.player() != null |
| 742 | + && event.player().self()) { |
| 743 | + playerLifecycleSelfJoinEmitted = true; |
| 744 | + } |
| 745 | + } |
| 746 | + |
| 747 | + if (!playerLifecycleSelfJoinEmitted && selfSnapshot != null) { |
| 748 | + emitPlayerLifecycleEvent("minecraft.player_join", selfSnapshot); |
| 749 | + playerLifecycleSelfJoinEmitted = true; |
| 750 | + } |
| 751 | + lastKnownSelfPlayer = selfSnapshot; |
| 752 | + } |
| 753 | + |
651 | 754 | private void tickTaskLifecycle() { |
652 | 755 | Optional<TaskSnapshot> active = taskRegistry.active(); |
653 | 756 | if (active.isEmpty()) { |
@@ -733,6 +836,65 @@ private void publishStatusEvent( |
733 | 836 | currentServer.publishEvent(session, ProtocolCodec.eventEnvelope("status.update", data)); |
734 | 837 | } |
735 | 838 |
|
| 839 | + private void onIncomingChatMessage(Text message, GameProfile sender) { |
| 840 | + MinecraftClient client = MinecraftClient.getInstance(); |
| 841 | + JsonObject authorPayload = null; |
| 842 | + if (sender != null) { |
| 843 | + String uuid = sender.getId() != null ? sender.getId().toString() : null; |
| 844 | + String name = sender.getName() != null && !sender.getName().isBlank() ? sender.getName() : "unknown"; |
| 845 | + boolean self = false; |
| 846 | + if (client != null && client.player != null && uuid != null && !uuid.isBlank()) { |
| 847 | + self = uuid.equals(client.player.getUuidAsString()); |
| 848 | + } |
| 849 | + authorPayload = playerPayload(uuid, name, self); |
| 850 | + } |
| 851 | + emitChatMessageEvent(message != null ? message.getString() : "", authorPayload); |
| 852 | + } |
| 853 | + |
| 854 | + private void onIncomingSystemMessage(Text message, boolean overlay) { |
| 855 | + JsonObject data = new JsonObject(); |
| 856 | + data.addProperty("message", message != null ? message.getString() : ""); |
| 857 | + data.addProperty("overlay", overlay); |
| 858 | + publishEvent("minecraft.system_message", data); |
| 859 | + } |
| 860 | + |
| 861 | + private void emitChatMessageEvent(String message, JsonObject author) { |
| 862 | + JsonObject data = new JsonObject(); |
| 863 | + data.addProperty("message", message == null ? "" : message); |
| 864 | + data.add("author", author != null ? author : JsonNull.INSTANCE); |
| 865 | + publishEvent("minecraft.chat_message", data); |
| 866 | + } |
| 867 | + |
| 868 | + private void emitPlayerLifecycleEvent(String eventName, PlayerLifecycleTracker.PlayerSnapshot player) { |
| 869 | + if (player == null) { |
| 870 | + return; |
| 871 | + } |
| 872 | + JsonObject data = new JsonObject(); |
| 873 | + data.add("player", playerPayload(player.uuid(), player.name(), player.self())); |
| 874 | + publishEvent(eventName, data); |
| 875 | + } |
| 876 | + |
| 877 | + private static JsonObject playerPayload(String uuid, String name, boolean self) { |
| 878 | + JsonObject payload = new JsonObject(); |
| 879 | + if (uuid != null && !uuid.isBlank()) { |
| 880 | + payload.addProperty("uuid", uuid); |
| 881 | + } else { |
| 882 | + payload.add("uuid", JsonNull.INSTANCE); |
| 883 | + } |
| 884 | + payload.addProperty("name", name == null || name.isBlank() ? "unknown" : name); |
| 885 | + payload.addProperty("self", self); |
| 886 | + return payload; |
| 887 | + } |
| 888 | + |
| 889 | + private static JsonObject currentPlayerPayload(MinecraftClient client) { |
| 890 | + if (client == null || client.player == null) { |
| 891 | + return null; |
| 892 | + } |
| 893 | + String uuid = client.player.getUuidAsString(); |
| 894 | + String name = client.player.getName() != null ? client.player.getName().getString() : "unknown"; |
| 895 | + return playerPayload(uuid, name, true); |
| 896 | + } |
| 897 | + |
736 | 898 | private void handlePausedUpdate(TaskSnapshot current, TaskLifecycleResolver.PauseStatus pauseStatus) { |
737 | 899 | String detail = pauseStatusDetail(pauseStatus); |
738 | 900 | Optional<TaskSnapshot> updated = taskRegistry.updateActiveDetail(detail); |
|
0 commit comments