Sistema de economía
El sistema de economía proporciona gestión de divisas validada por el servidor, una casa de subastas (Global Trade Link) y transacciones de tienda del lado del servidor. Estos servicios se inicializan automáticamente por NakamaManager (el Global Trade Station, Gts, y la sincronización del inventario, Inventory, viven a su lado).

El saldo de la esquina no es un número que guarde el cliente: es lo último que el servicio de cartera supo del servidor, que además es lo único que puede cambiarlo. Los filtros (categoría, precio, orden) van dentro de la consulta: el explorador nunca descarga un catálogo para filtrarlo localmente, y eso es lo que mantiene barato un mercado grande.

Los intercambios directos pasan por las cuatro etapas que se nombran arriba: invite, offering, confirm, done. Los dos candados de abajo a la izquierda tienen que cerrarse antes de que Confirm Trade signifique algo, y el servidor es dueño de la transición: un cliente que dice «done» fuera de turno es rechazado, que es toda la razón por la que la máquina de estados vive en el servidor y no en la ventana que estás mirando.
Descripción general de la arquitectura
| Clase | Archivo | Responsabilidad |
|---|---|---|
WalletService | Economy/EconomyServices.cs | Seguimiento de saldo multidivisa via la billetera de Nakama |
ShopService | Economy/EconomyServices.cs | Compra/venta en tienda NPC validada por el servidor |
GTLService | Economy/EconomyServices.cs | Casa de subastas Global Trade Link para comercio jugador a jugador |
Acceda a los tres via NakamaManager.Instance:
var wallet = NakamaManager.Instance.Wallet;
var shop = NakamaManager.Instance.Shop;
var gtl = NakamaManager.Instance.GTL;
WalletService
WalletService gestiona el saldo multidivisa del jugador. El sistema de billetera del lado del servidor de Nakama asegura que los cambios de divisa sean atómicos y a prueba de manipulación.
Consultar saldos
// Refresh from server
await wallet.RefreshBalanceAsync(userId);
// Get balance for a specific currency (defaults to "gold")
long gold = wallet.GetBalance("gold");
long premiumGems = wallet.GetBalance("gems");
// Access the full currency dictionary
Dictionary<string, long> currencies = wallet.Currencies;
Eventos
wallet.OnBalanceChanged += (long newBalance) => { /* gold changed */ };
wallet.OnCurrencyChanged += (string currencyId, long oldValue, long newValue) =>
{
Debug.Log($"{currencyId}: {oldValue} -> {newValue}");
};
Autoridad del servidor
Cuando OnlineConfig.EnableServerAuthority es true, todos los cambios de dinero pasan por el OnlineCommandBridge:
// This is called internally by CommandGraph nodes (GivePlayerMoney, etc.)
bool approved = await bridge.RequestGiveMoneyAsync(amount: 500, source: "npc_reward");
El servidor valida el origen y el monto antes de aplicar el cambio. Esto previene los exploits de duplicación de dinero.
ShopService
ShopService maneja las transacciones de tienda NPC con validación del lado del servidor.
Comprar objetos
bool success = await shop.BuyFromShopAsync(
shopId: "pokemart_viridian",
itemId: "potion",
quantity: 5);
El servidor valida:
- Que la tienda exista y contenga el objeto.
- Que el jugador tenga suficiente divisa.
- Que el objeto esté disponible (sin agotarse para objetos limitados).
Vender objetos
long revenue = await shop.SellToShopAsync(
itemId: "nugget",
quantity: 1);
// revenue = the amount of gold received
El servidor valida que el jugador realmente tenga el objeto en su inventario antes de acreditar la divisa.
Global Trade Link (GTL)
El GTL es un sistema de casa de subastas que permite a los jugadores listar monstruos y objetos a la venta para otros jugadores globalmente. Funciona de forma similar al GTS/GTE de los juegos populares de captura de monstruos, pero con soporte completo de objetos y comisiones de listado.
Configuración
| Campo de config | Por defecto | Descripción |
|---|---|---|
GTLListingFee | 0.05 (5%) | Porcentaje de comisión deducido del precio de listado |
GTLListingDurationHours | 48 | Horas antes de que expire un listado |
MaxGTLListings | 20 | Máximo de listados activos por jugador |
Listar un monstruo
GTLListing listing = await gtl.ListMonsterAsync(
monsterId: "monster_uuid_here",
price: 50000);
// listing.Id -- unique listing ID
// listing.Fee -- the listing fee (price * GTLListingFee)
// listing.ExpiresAt -- Unix timestamp when the listing expires
// listing.Status -- GTLListingStatus.Active
Listar un objeto
GTLListing listing = await gtl.ListItemAsync(
itemId: "rare_candy",
quantity: 10,
price: 25000);
Buscar en el GTL
var filter = new GTLSearchFilter
{
Query = "fire",
Type = GTLItemType.Monster,
MinPrice = 1000,
MaxPrice = 100000,
SortBy = "price",
Ascending = true,
Page = 0,
PageSize = 20
};
List<GTLListing> results = await gtl.SearchAsync(filter);
GTLSearchFilter soporta:
Query-- búsqueda de texto en el nombre del objeto/monstruo.Type-- filtrar porGTLItemType.MonsteroGTLItemType.Item.MinPrice/MaxPrice-- filtro de rango de precio.SortBy-- nombre del campo de ordenamiento.Ascending-- dirección del ordenamiento.Page/PageSize-- paginación (tamaño de página por defecto: 20).
Comprar un listado
bool success = await gtl.BuyListingAsync(listingId);
Cancelar un listado
bool cancelled = await gtl.CancelListingAsync(listingId);
Ver tus listados
List<GTLListing> myListings = await gtl.GetMyListingsAsync();
Eventos del GTL
gtl.OnListingSold += (GTLListing listing) =>
{
Debug.Log($"Your {listing.ItemName} sold for {listing.Price}!");
};
gtl.OnListingExpired += (GTLListing listing) =>
{
Debug.Log($"Listing for {listing.ItemName} expired.");
};
Modelo de datos del GTL
La clase GTLListing contiene:
| Campo | Tipo | Descripción |
|---|---|---|
Id | string | ID único del listado |
SellerId | string | ID de usuario del vendedor |
SellerName | string | Nombre visible del vendedor |
Type | GTLItemType | Monster o Item |
ItemId | string | ID del monstruo u objeto |
ItemName | string | Nombre visible |
Quantity | int | Cantidad (para objetos) |
Price | long | Precio pedido |
Fee | long | Comisión de listado deducida |
Status | GTLListingStatus | Active, Sold, Expired o Cancelled |
ExpiresAt | long | Timestamp Unix (milisegundos) |
CreatedAt | long | Timestamp Unix (milisegundos) |
UI de cliente
El GTL incluye una UI de cliente real y conectada. GTLBrowserUgui
(Assets/OpenMon/Online/UI/UGUI/GTLBrowserUgui.cs) es un explorador de casa de subastas UGUI
que llama a NakamaManager.Instance.GTL directamente — SearchAsync,
GetMyListingsAsync, BuyListingAsync — y se suscribe a OnListingSold. Recae
en un estado honesto "Offline — the GTL is unavailable" cuando no está conectado.
Junto a los métodos que devuelven bool documentados arriba, GTLService también expone
variantes *WithResultAsync (SearchListingsAsync, BuyListingWithResultAsync,
CancelListingWithResultAsync, …) que devuelven OnlineResult<T> para que quien llama
pueda mostrar un error del servidor en lugar de un simple false.
GTS — Global Trade Station
Un segundo intercambio, el GTS, es un comercio asíncrono de monstruo por monstruo. GtsService (NakamaManager.Instance.Gts, en Economy/EconomyServices.cs) es su transporte de cliente sobre los RPC del servidor gts_deposit, gts_search y gts_withdraw:
| Método | RPC | Qué hace |
|---|---|---|
DepositAsync / DepositWithResultAsync | gts_deposit | Deposita en custodia un monstruo del equipo sacándolo del equipo del jugador y publica lo que se pide a cambio (un GtsWantSpec). El servidor busca de inmediato un depósito que satisfaga a ambas partes: si lo hay, los dos monstruos se intercambian de forma atómica en esa misma llamada; si no, el depósito queda en el tablero |
SearchAsync / SearchWithResultAsync | gts_search | Lee el tablero de depósitos activos y no vencidos (solo lectura: aquí no se intercambia nada). Lo que ofreces y lo que quieres pueden prefiltrar el tablero |
WithdrawAsync / WithdrawWithResultAsync | gts_withdraw | Recupera uno de tus propios depósitos activos |
Igual que el GTL, cada operación tiene una sobrecarga para la interfaz que degrada a una lista vacía o a false, y una sobrecarga *WithResultAsync que devuelve un OnlineResult<T> con el error del servidor. La ficha comparable de un monstruo se deriva en el servidor a partir del monstruo en custodia, así que no se confía en ninguna ficha enviada por el cliente. IGtsTransport es la interfaz que hay detrás, de modo que una interfaz de usuario se puede probar contra un doble falso.
GtsBrowserUgui (Assets/OpenMon/Online/UI/UGUI/GtsBrowserUgui.cs) es la interfaz UGUI: ensambla un GtsWantSpec a partir de sus controles de filtro y usa el GtsMonsterMatcher puro (sin Unity, sin Nakama) para filtrar el tablero de depósitos.
Balanceo de la economía
El diseño de economía de MCE incluye varios sumideros de divisa integrados:
- Comisiones de listado del GTL (5% por defecto) -- retira divisa de la circulación con cada comercio.
- Precios de tienda validados por el servidor -- el servidor controla las proporciones de compra/venta, previniendo el arbitraje.
- Recompensas de batalla controladas del lado del servidor -- el servidor determina la EXP, el dinero y los drops de objetos.
- Auditoría de inventario anti-trampas -- el
InventoryAuditordetecta la duplicación de objetos y las discrepancias de cantidad (consulte la sección Anti-Cheat para detalles).
Medidas anti-fraude
Cuando EnableServerAuthority está activo:
- Todos los cambios de divisa pasan por
OnlineCommandBridge.RequestGiveMoneyAsync(), que llama al RPCmce_give_money. El servidor valida el origen (recompensa de batalla, venta en tienda, misión, etc.) antes de aplicar. - Todos los cambios de objetos pasan por
OnlineCommandBridge.RequestGiveItemAsync(), que llama al RPCmce_give_item. - El
InventoryAuditortoma instantáneas después de cada acción validada y las compara con los inventarios reportados por el cliente. Las discrepancias disparan eventosAuditViolation. - El
ActionRateLimiterpreviene los exploits de compra/venta a ritmo rápido (0.3s por defecto según se configura en AntiCheatManager; el valor por defecto de la clase es 0.5s).