Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions src/main/java/com/hfstudio/guidenh/ClientProxy.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@
import net.minecraftforge.client.ClientCommandHandler;
import net.minecraftforge.common.MinecraftForge;

import com.hfstudio.guidenh.bridge.GuideNhRuntimeBridge;
import com.hfstudio.guidenh.bridge.GuideNhRuntimeBridgeSettings;
import com.hfstudio.guidenh.client.RegionWandRenderer;
import com.hfstudio.guidenh.client.command.GuideNhClientBridgeController;
import com.hfstudio.guidenh.client.command.GuideNhClientCommand;
import com.hfstudio.guidenh.client.hotkey.CycleRegionWandModeHotkey;
import com.hfstudio.guidenh.client.hotkey.OpenGuideHomeHotkey;
import com.hfstudio.guidenh.client.hotkey.OpenGuideHotkey;
import com.hfstudio.guidenh.client.hotkey.OpenSceneEditorHotkey;
import com.hfstudio.guidenh.config.ModConfig;
import com.hfstudio.guidenh.guide.internal.GuideDevWatcherPump;
import com.hfstudio.guidenh.guide.internal.GuideDevelopmentResourcePackWatcher;
import com.hfstudio.guidenh.guide.internal.GuideME;
Expand Down Expand Up @@ -44,6 +47,8 @@

public class ClientProxy extends CommonProxy {

private final GuideNhRuntimeBridge runtimeBridge = new GuideNhRuntimeBridge();

@Override
public void preInit(FMLPreInitializationEvent event) {
super.preInit(event);
Expand Down Expand Up @@ -79,6 +84,24 @@ public void init(FMLInitializationEvent event) {
MinecraftForge.EVENT_BUS.register(new RegionWandRenderer());
GuideWarmupPump.init();
MinecraftForge.EVENT_BUS.register(this);
GuideNH.LOG.info(
"GuideNH runtime bridge configuration loaded. enabled={}, hostConfigured={}, port={}, tokenConfigured={}",
ModConfig.runtimeBridge.enabled,
ModConfig.runtimeBridge.host != null && !ModConfig.runtimeBridge.host.trim()
.isEmpty(),
ModConfig.runtimeBridge.port,
ModConfig.runtimeBridge.token != null && !ModConfig.runtimeBridge.token.isEmpty());
runtimeBridge.start(
new GuideNhRuntimeBridgeSettings(
ModConfig.runtimeBridge.enabled,
ModConfig.runtimeBridge.host,
ModConfig.runtimeBridge.port,
ModConfig.runtimeBridge.token,
ModConfig.runtimeBridge.maxMessageBytes,
ModConfig.runtimeBridge.maxPageSize,
ModConfig.runtimeBridge.maxSubscriptions,
ModConfig.runtimeBridge.maxConnections,
ModConfig.runtimeBridge.maxDeltaEntries));
}

@Override
Expand All @@ -96,6 +119,8 @@ public void completeInit(FMLLoadCompleteEvent event) {

@SubscribeEvent
public void onClientDisconnect(FMLNetworkEvent.ClientDisconnectionFromServerEvent event) {
GuideNH.LOG.info("Minecraft client disconnected. Stopping GuideNH runtime bridge session state");
runtimeBridge.stop();
GuideME.closeSearch();
GuideScreenMemory.clear();
GuideScreenHomeHistory.shared()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.hfstudio.guidenh.bridge;

import com.hfstudio.guidenh.GuideNH;

public class GuideNhRuntimeBridge {

private GuideNhRuntimeBridgeServer server;

public void start(GuideNhRuntimeBridgeSettings settings) {
stop();
if (!settings.canStart()) {
GuideNH.LOG.info(
"GuideNH runtime bridge start skipped. enabled={}, hostConfigured={}, portConfigured={}, tokenConfigured={}",
settings.isEnabled(),
!settings.getHost()
.isEmpty(),
settings.getPort() > 0,
!settings.getToken()
.isEmpty());
return;
}
GuideNH.LOG.info(
"Starting GuideNH runtime bridge. host={}, port={}, maxConnections={}, maxMessageBytes={}, maxPageSize={}, maxSubscriptions={}, maxDeltaEntries={}",
settings.getHost(),
settings.getPort(),
settings.getMaxConnections(),
settings.getMaxMessageBytes(),
settings.getMaxPageSize(),
settings.getMaxSubscriptions(),
settings.getMaxDeltaEntries());
server = new GuideNhRuntimeBridgeServer(settings);
server.start();
}

public void stop() {
if (server != null) {
GuideNH.LOG.info("Stopping GuideNH runtime bridge");
server.stop();
server = null;
}
}

public boolean isRunning() {
return server != null && server.isRunning();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package com.hfstudio.guidenh.bridge;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicBoolean;

import com.hfstudio.guidenh.GuideNH;
import com.hfstudio.guidenh.bridge.preview.ItemPreviewCache;
import com.hfstudio.guidenh.bridge.preview.ItemPreviewSearchService;
import com.hfstudio.guidenh.bridge.preview.ItemPreviewService;
import com.hfstudio.guidenh.bridge.preview.RuntimePreviewFacade;
import com.hfstudio.guidenh.bridge.protocol.BridgeMessageCodec;
import com.hfstudio.guidenh.bridge.protocol.BridgeProtocolLimits;
import com.hfstudio.guidenh.bridge.security.BridgeTokenAuthenticator;
import com.hfstudio.guidenh.bridge.semantic.SemanticProviderRegistry;
import com.hfstudio.guidenh.bridge.semantic.providers.RuntimeSemanticProviders;
import com.hfstudio.guidenh.bridge.transport.RuntimeBridgeConnection;

public class GuideNhRuntimeBridgeServer {

private final GuideNhRuntimeBridgeSettings settings;
private final BridgeProtocolLimits limits;
private final BridgeMessageCodec messageCodec;
private final BridgeTokenAuthenticator authenticator;
private final SemanticProviderRegistry registry = new SemanticProviderRegistry();
private final RuntimePreviewFacade previewFacade;
private final Set<RuntimeBridgeConnection> connections = Collections.synchronizedSet(new HashSet<>());
private final ExecutorService executor = Executors.newCachedThreadPool(new RuntimeBridgeThreadFactory());
private final AtomicBoolean running = new AtomicBoolean();
private ServerSocket serverSocket;

public GuideNhRuntimeBridgeServer(GuideNhRuntimeBridgeSettings settings) {
this.settings = settings;
this.limits = new BridgeProtocolLimits(
settings.getMaxMessageBytes(),
settings.getMaxPageSize(),
settings.getMaxSubscriptions(),
settings.getMaxConnections(),
settings.getMaxDeltaEntries());
this.messageCodec = new BridgeMessageCodec(limits);
this.authenticator = new BridgeTokenAuthenticator(settings.getToken());
RuntimeSemanticProviders.registerBaseline(registry);
ItemPreviewCache previewCache = new ItemPreviewCache(256);
ItemPreviewSearchService previewSearchService = new ItemPreviewSearchService();
ItemPreviewService previewService = new ItemPreviewService(previewCache, limits);
this.previewFacade = new RuntimePreviewFacade(previewSearchService, previewService);
}

public void start() {
if (!settings.canStart() || !running.compareAndSet(false, true)) {
return;
}
try {
GuideNH.LOG.info("Binding GuideNH runtime bridge server to {}:{}", settings.getHost(), settings.getPort());
serverSocket = new ServerSocket();
serverSocket.bind(new InetSocketAddress(settings.getHost(), settings.getPort()));
executor.execute(this::acceptConnections);
GuideNH.LOG.info("GuideNH runtime bridge started at ws://{}:{}", settings.getHost(), settings.getPort());
} catch (IOException e) {
running.set(false);
closeServerSocket();
GuideNH.LOG.warn("Failed to start GuideNH runtime bridge", e);
}
}

public void stop() {
if (!running.getAndSet(false)) {
return;
}
GuideNH.LOG.info("GuideNH runtime bridge server stopping");
closeServerSocket();
List<RuntimeBridgeConnection> snapshot;
synchronized (connections) {
snapshot = new ArrayList<>(connections);
connections.clear();
}
for (RuntimeBridgeConnection connection : snapshot) {
connection.close();
}
executor.shutdownNow();
}

public boolean isRunning() {
return running.get();
}

private void acceptConnections() {
while (running.get()) {
try {
Socket socket = serverSocket.accept();
String remoteAddress = describeRemote(socket);
GuideNH.LOG.info("GuideNH runtime bridge accepted socket from {}", remoteAddress);
if (connections.size() >= limits.getMaxConnections()) {
GuideNH.LOG.warn(
"GuideNH runtime bridge rejected socket from {} because maxConnections={} has been reached",
remoteAddress,
limits.getMaxConnections());
socket.close();
continue;
}
RuntimeBridgeConnection connection = new RuntimeBridgeConnection(
socket,
messageCodec,
authenticator,
registry,
previewFacade,
limits,
this::handleClosedConnection);
connections.add(connection);
GuideNH.LOG.info(
"GuideNH runtime bridge starting session for {}. activeConnections={}",
remoteAddress,
connections.size());
executor.execute(connection);
} catch (IOException e) {
if (running.get()) {
GuideNH.LOG.warn("GuideNH runtime bridge accept loop failed", e);
}
}
}
}

private void closeServerSocket() {
try {
if (serverSocket != null) {
serverSocket.close();
}
} catch (IOException ignored) {}
}

private String describeRemote(Socket socket) {
if (socket == null || socket.getRemoteSocketAddress() == null) {
return "unknown";
}
return String.valueOf(socket.getRemoteSocketAddress());
}

private void handleClosedConnection(RuntimeBridgeConnection connection) {
connections.remove(connection);
GuideNH.LOG.info(
"GuideNH runtime bridge session closed for {}. activeConnections={}",
connection.getRemoteAddress(),
connections.size());
}

public static class RuntimeBridgeThreadFactory implements ThreadFactory {

private int nextThreadId;

@Override
public Thread newThread(Runnable runnable) {
Thread thread = new Thread(runnable, "GuideNH-RuntimeBridge-" + nextThreadId++);
thread.setDaemon(true);
return thread;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.hfstudio.guidenh.bridge;

public class GuideNhRuntimeBridgeSettings {

private final boolean enabled;
private final String host;
private final int port;
private final String token;
private final int maxMessageBytes;
private final int maxPageSize;
private final int maxSubscriptions;
private final int maxConnections;
private final int maxDeltaEntries;

public GuideNhRuntimeBridgeSettings(boolean enabled, String host, int port, String token, int maxMessageBytes,
int maxPageSize, int maxSubscriptions, int maxConnections, int maxDeltaEntries) {
this.enabled = enabled;
this.host = host == null ? "" : host.trim();
this.port = port;
this.token = token == null ? "" : token;
this.maxMessageBytes = maxMessageBytes;
this.maxPageSize = maxPageSize;
this.maxSubscriptions = maxSubscriptions;
this.maxConnections = maxConnections;
this.maxDeltaEntries = maxDeltaEntries;
}

public boolean canStart() {
return enabled && !host.isEmpty() && port > 0 && port <= 65535 && !token.isEmpty();
}

public boolean isEnabled() {
return enabled;
}

public String getHost() {
return host;
}

public int getPort() {
return port;
}

public String getToken() {
return token;
}

public int getMaxMessageBytes() {
return maxMessageBytes;
}

public int getMaxPageSize() {
return maxPageSize;
}

public int getMaxSubscriptions() {
return maxSubscriptions;
}

public int getMaxConnections() {
return maxConnections;
}

public int getMaxDeltaEntries() {
return maxDeltaEntries;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.hfstudio.guidenh.bridge.preview;

import java.util.LinkedHashMap;
import java.util.Map;

public class ItemPreviewCache {

private final int maxEntries;
private final Map<ItemPreviewCacheKey, ItemPreviewPayload> cache;

public ItemPreviewCache(int maxEntries) {
this.maxEntries = Math.max(1, maxEntries);
this.cache = new LinkedHashMap<ItemPreviewCacheKey, ItemPreviewPayload>(16, 0.75f, true) {

@Override
protected boolean removeEldestEntry(Map.Entry<ItemPreviewCacheKey, ItemPreviewPayload> eldest) {
return size() > ItemPreviewCache.this.maxEntries;
}
};
}

public synchronized ItemPreviewPayload get(ItemPreviewCacheKey key) {
return cache.get(key);
}

public synchronized void put(ItemPreviewCacheKey key, ItemPreviewPayload value) {
cache.put(key, value);
}
}
Loading