Saltar al contenido principal

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 explorador del Global Trade Link

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.

La ventana de intercambio entre jugadores

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​

ClaseArchivoResponsabilidad
WalletServiceEconomy/EconomyServices.csSeguimiento de saldo multidivisa via la billetera de Nakama
ShopServiceEconomy/EconomyServices.csCompra/venta en tienda NPC validada por el servidor
GTLServiceEconomy/EconomyServices.csCasa 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.

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 configPor defectoDescripción
GTLListingFee0.05 (5%)Porcentaje de comisión deducido del precio de listado
GTLListingDurationHours48Horas antes de que expire un listado
MaxGTLListings20Má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 por GTLItemType.Monster o GTLItemType.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:

CampoTipoDescripción
IdstringID único del listado
SellerIdstringID de usuario del vendedor
SellerNamestringNombre visible del vendedor
TypeGTLItemTypeMonster o Item
ItemIdstringID del monstruo u objeto
ItemNamestringNombre visible
QuantityintCantidad (para objetos)
PricelongPrecio pedido
FeelongComisión de listado deducida
StatusGTLListingStatusActive, Sold, Expired o Cancelled
ExpiresAtlongTimestamp Unix (milisegundos)
CreatedAtlongTimestamp 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étodoRPCQué hace
DepositAsync / DepositWithResultAsyncgts_depositDeposita 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 / SearchWithResultAsyncgts_searchLee 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 / WithdrawWithResultAsyncgts_withdrawRecupera 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:

  1. Comisiones de listado del GTL (5% por defecto) -- retira divisa de la circulación con cada comercio.
  2. Precios de tienda validados por el servidor -- el servidor controla las proporciones de compra/venta, previniendo el arbitraje.
  3. Recompensas de batalla controladas del lado del servidor -- el servidor determina la EXP, el dinero y los drops de objetos.
  4. Auditoría de inventario anti-trampas -- el InventoryAuditor detecta 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 RPC mce_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 RPC mce_give_item.
  • El InventoryAuditor toma instantáneas después de cada acción validada y las compara con los inventarios reportados por el cliente. Las discrepancias disparan eventos AuditViolation.
  • El ActionRateLimiter previene 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).