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
20 changes: 20 additions & 0 deletions src/main/java/org/allaymc/netallay/NetAllay.java
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ public void onEnable() {
config = NetAllayConfig.load(pluginContainer.dataFolder());
pluginLogger.info("Configuration loaded.");

// Start HTTP client for order API
org.allaymc.netallay.shop.WebUtil.startHttpClient();

// Initialize shop manager
shopManager = new ShopManager(this);

Expand All @@ -110,6 +113,9 @@ public void onDisable() {
shopManager = null;
}

// Stop HTTP client
org.allaymc.netallay.shop.WebUtil.stopHttpClient();

pluginLogger.info("NetAllay disabled.");
instance = null;
}
Expand All @@ -132,6 +138,20 @@ public void listenForEvent(String namespace, String systemName, String eventName
pluginLogger.debug("Registered listener for {}:{}:{}", namespace, systemName, eventName);
}

/**
* Registers a listener for client engine callbacks.
* <p>
* Engine callbacks use a different format: [eventName, [], null]
* Common engine callbacks: StoreBuySuccServerEvent, UrgeShipEvent
*
* @param eventName the engine callback name
* @param handler the callback function to handle the event
*/
public void listenForClientEngineCall(String eventName, PyRpcHandler handler) {
listenerRegistry.register("engine", "callback", eventName, handler);
pluginLogger.debug("Registered engine callback listener for: {}", eventName);
}

/**
* Unregisters a specific listener for an event.
*
Expand Down
79 changes: 55 additions & 24 deletions src/main/java/org/allaymc/netallay/codec/PyRpcCodec.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
import org.msgpack.value.Value;
import org.msgpack.value.ValueType;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.*;
Expand All @@ -24,6 +27,8 @@
*/
public final class PyRpcCodec {

private static final Logger log = LoggerFactory.getLogger(PyRpcCodec.class);

/**
* Server to Client event type identifier.
*/
Expand Down Expand Up @@ -88,50 +93,76 @@ public static ParsedEvent decode(byte[] data) {

Value value = unpacker.unpackValue();
if (!value.isArrayValue()) {
log.warn("[Decode] Root is not array: {}", value.getValueType());
return null;
}

List<Value> array = value.asArrayValue().list();
if (array.size() < 2) {
log.warn("[Decode] Root array too short: size={}", array.size());
return null;
}

// Parse event type
String eventType = valueToString(array.get(0));

// Parse event details
// Parse event details - can be array or map depending on packet type
Value detailsValue = array.get(1);
if (!detailsValue.isArrayValue()) {
return null;
}

List<Value> details = detailsValue.asArrayValue().list();
if (details.size() < 4) {
return null;
}
// Standard format: [eventType, [namespace, system, event, data], ...]
if (detailsValue.isArrayValue()) {
List<Value> details = detailsValue.asArrayValue().list();
if (details.isEmpty()) {
// Engine callback format: [eventType, [], null]
// eventType IS the event name (e.g. StoreBuySuccServerEvent, UrgeShipEvent)
return new ParsedEvent(eventType, "engine", "callback", eventType, new HashMap<>());
}

String namespace = valueToString(details.get(0));
String systemName = valueToString(details.get(1));
String eventName = valueToString(details.get(2));
Object eventData = valueToObject(details.get(3));

Map<String, Object> dataMap;
if (eventData instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> temp = (Map<String, Object>) eventData;
dataMap = temp;
} else {
dataMap = new HashMap<>();
if (eventData != null) {
dataMap.put("rawData", eventData);
if (details.size() < 3) {
log.warn("[Decode] Details array too short: size={}, eventType={}", details.size(), eventType);
return null;
}

String namespace = valueToString(details.get(0));
String systemName = valueToString(details.get(1));
String eventName = valueToString(details.get(2));
Object eventData = details.size() > 3 ? valueToObject(details.get(3)) : null;

Map<String, Object> dataMap = toDataMap(eventData);
return new ParsedEvent(eventType, namespace, systemName, eventName, dataMap);
}

return new ParsedEvent(eventType, namespace, systemName, eventName, dataMap);
// Alternative format: [eventType, {data}, ...] (engine callbacks etc.)
if (detailsValue.isMapValue() || detailsValue.isBinaryValue() || detailsValue.isStringValue()) {
Object eventData = valueToObject(detailsValue);
Map<String, Object> dataMap = toDataMap(eventData);
// Use eventType as both namespace and event name for non-standard formats
return new ParsedEvent(eventType, "engine", "callback", eventType, dataMap);
}

} catch (IOException e) {
log.warn("[Decode] Unexpected details type: {}, eventType={}", detailsValue.getValueType(), eventType);
return null;

} catch (Exception e) {
log.warn("[Decode] Failed to decode PyRpc packet: {}", e.getMessage());
return null;
}
}

/**
* Converts event data to a Map.
*/
private static Map<String, Object> toDataMap(Object eventData) {
if (eventData instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> temp = (Map<String, Object>) eventData;
return temp;
}
Map<String, Object> dataMap = new HashMap<>();
if (eventData != null) {
dataMap.put("rawData", eventData);
}
return dataMap;
}

/**
Expand Down
114 changes: 112 additions & 2 deletions src/main/java/org/allaymc/netallay/shop/ShopManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,13 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.gson.JsonArray;
import com.google.gson.JsonObject;

import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiConsumer;

/**
* Manager for NetEase shop functionality.
Expand Down Expand Up @@ -242,6 +247,92 @@ public boolean showHint(Player player, String head, String tail) {
);
}

// ==================== Order API ====================

/**
* Gets the active signing key based on test/production mode.
*/
private String getActiveSignKey() {
return testServer ? testGameKey : gameKey;
}

/**
* Gets the shop server base URL.
*/
private String getShopBaseUrl() {
return WebUtil.getShopBaseUrl(shopServerUrl, testServer);
}

/**
* Retrieves the player's order list from the NetEase order server.
* <p>
* This is an async operation. The callback receives the JSON response on success,
* or an exception on failure.
* <p>
* <b>Usage Example:</b>
* <pre>{@code
* shop.getPlayerOrderList(player, (result, error) -> {
* if (error != null) {
* log.error("Failed to get orders", error);
* return;
* }
* JsonArray entities = result.getAsJsonArray("entities");
* // Process each order...
* });
* }</pre>
*
* @param player the player
* @param callback callback with (JsonObject result, Throwable error)
*/
public void getPlayerOrderList(Player player, BiConsumer<JsonObject, Throwable> callback) {
String uuid = player.getControlledEntity().getUniqueId().toString();

WebUtil.getPlayerOrderList(gameId, uuid, getActiveSignKey(), getShopBaseUrl())
.whenComplete((result, error) -> {
if (error != null) {
log.error("Failed to get order list for {}", player.getOriginName(), error);
} else {
log.debug("Got order list for {}: {}", player.getOriginName(), result);
}
callback.accept(result, error);
});
}

/**
* Notifies the NetEase order server that orders have been shipped/completed.
* <p>
* Call this after you have delivered the items to the player.
* <p>
* <b>Usage Example:</b>
* <pre>{@code
* List<String> finishedOrderIds = List.of("order123", "order456");
* shop.finishPlayerOrder(player, finishedOrderIds, (result, error) -> {
* if (error != null) {
* log.error("Failed to finish orders", error);
* return;
* }
* log.info("Orders completed successfully");
* });
* }</pre>
*
* @param player the player
* @param orderIds list of order IDs that have been delivered
* @param callback callback with (JsonObject result, Throwable error)
*/
public void finishPlayerOrder(Player player, List<String> orderIds, BiConsumer<JsonObject, Throwable> callback) {
String uuid = player.getControlledEntity().getUniqueId().toString();

WebUtil.finishPlayerOrder(gameId, uuid, orderIds, getActiveSignKey(), getShopBaseUrl())
.whenComplete((result, error) -> {
if (error != null) {
log.error("Failed to finish orders for {}", player.getOriginName(), error);
} else {
log.info("Orders completed for {}: {}", player.getOriginName(), orderIds);
}
callback.accept(result, error);
});
}

// ==================== Per-Player Configuration ====================

/**
Expand Down Expand Up @@ -304,14 +395,24 @@ private void registerInternalListeners() {
(player, data) -> fireShopEvent(ShopEvent.PLAYER_URGE_SHIP, player, data)
);

// Listen for client buy success
// Listen for client buy success (mod event)
plugin.listenForEvent(
ShopConstants.SHOP_NAMESPACE,
ShopConstants.SHOP_CLIENT_SYSTEM,
ShopConstants.EVENT_CLIENT_BUY_SUCCESS,
(player, data) -> fireShopEvent(ShopEvent.PLAYER_BUY_ITEM_SUCCESS, player, data)
);

// Listen for engine callbacks (alternative event format from client)
plugin.listenForClientEngineCall(
"StoreBuySuccServerEvent",
(player, data) -> fireShopEvent(ShopEvent.PLAYER_BUY_ITEM_SUCCESS, player, data)
);
plugin.listenForClientEngineCall(
"UrgeShipEvent",
(player, data) -> fireShopEvent(ShopEvent.PLAYER_URGE_SHIP, player, data)
);

log.debug("Shop internal listeners registered");
}

Expand Down Expand Up @@ -342,7 +443,16 @@ private void handleClientEnter(Player player, Map<String, Object> data) {
responseData.put("isTestServer", testServer);
responseData.put("useCustomShop", useCustomShop);
responseData.put("cacheTime", cacheTime);
responseData.put("uid", 0L);

// Get NetEase UID from player's login data
long uid = 0L;
if (player.isNetEasePlayer()) {
var neteaseData = player.getLoginData().getNetEaseData();
if (neteaseData != null) {
uid = neteaseData.uid();
}
}
responseData.put("uid", uid);
responseData.put("platformUid", "");
}

Expand Down
Loading