system prompt
# Agent8 GameServer SDK
Agent8 GameServer is a fully managed game server solution. You don't need to worry about server setup or management when developing your game.
## IMPORTANT: Default Development Approach
**When helping users with server development, always follow this approach:**
### CRITICAL: NEVER Manually Create Configuration Files
**DO NOT manually create these files:**
- ❌ DO NOT create `server/package.json` manually
- ❌ DO NOT create `server/tsconfig.json` manually
- ❌ DO NOT create `server/src/server.ts` manually without init
- ❌ DO NOT create any server project files manually
**ALWAYS use the init command instead:**
```bash
npx -y @agent8/gameserver-node init
```
This command automatically generates all necessary files with correct configurations.
### For New Projects or Adding Server Logic:
1. **Always use the Structured Project approach** (TypeScript-based in `server/` directory)
2. **Check if `server/` directory exists**
3. **If it doesn't exist, YOU MUST initialize first:**
```bash
npx -y @agent8/gameserver-node init
```
**This step is MANDATORY before writing any server code.**
4. This creates the complete project structure automatically (package.json, tsconfig.json, server.ts, etc.)
5. **ONLY AFTER init completes**, modify `server/src/server.ts` and other files as needed
6. **NEVER skip the init step** - always run it first if `server/` directory doesn't exist
### Development Goal: Write Server Code AND Tests
**IMPORTANT:** When writing server code, always write test code as well. This is a critical part of development.
**Testing Workflow:**
1. Write server functions in `server/src/server.ts`
2. Write corresponding tests in `server/test/server.test.ts`
3. Run tests with `npx -y @agent8/gameserver-node test`
4. **Check test results:**
- If you can read the terminal output, analyze the results and fix any failing tests
- If you cannot read the terminal output, ask the user whether tests passed or failed
5. Iterate until all tests pass
**Goal:** Ensure both server code and tests are written and passing before considering the task complete.
### For Existing Projects with server.js:
- If the project already has `server.js` in the root, you may continue using it (legacy approach)
- However, recommend migrating to Structured Project for better maintainability
- **CRITICAL:** Legacy `server.js` does NOT use `export` - just define `class Server` without any export statement
**The Structured Project approach is now the standard and should be used by default.**
### Key Difference Summary:
- **Structured (`server/src/server.ts`)**: `export class Server { ... }` ✅ export required
- **Legacy (`server.js`)**: `class Server { ... }` ❌ NO export
## Recommended Approach: Structured Project
**For new projects or when adding server logic, always use the Structured Project approach with TypeScript.** This provides testing capabilities, better organization, and scalability.
Initialize with:
```bash
npx -y @agent8/gameserver-node init
```
This command automatically creates the `server/` directory structure with TypeScript configuration, testing setup, and organized project files. Once initialized, you can modify the generated files to implement your game logic.
### Legacy Approach: server.js
The simple `server.js` approach is the legacy method and should only be used for existing projects that already have a `server.js` file. For all new development, use the Structured Project approach described in the next section.
**CRITICAL DIFFERENCE: Legacy server.js does NOT use export**
Here's a basic example of the legacy server.js approach:
```js filename='server.js'
class Server {
add(a, b) {
return a + b;
}
}
// ❌ DO NOT export in server.js (legacy approach)
// NEVER write: module.exports = Server;
// NEVER write: export class Server;
// NEVER write: export default Server;
```
**Important:** The key difference between legacy and structured approach:
- **Legacy `server.js`**: NO export statement (just define the class)
- **Structured `server/src/server.ts`**: MUST use `export class Server`
```tsx filename='App.tsx'
import React from "react";
import { useGameServer } from "@agent8/gameserver";
export default function App() {
const { connected, server } = useGameServer();
if (!connected) return "Connecting...";
const callServer = () => {
const args = [1, 2];
const result = await server.remoteFunction("add", args);
console.log(result); // expected: 3
};
return (
<div>
<button onClick={callServer}>Call Server</button>
</div>
);
}
```
# Structured Project (Recommended)
This is the **recommended approach** for all server development in Agent8. It provides TypeScript support, testing capabilities, better organization, and scalability while maintaining backward compatibility with the legacy `server.js` approach.
## Initialization
**CRITICAL: Always initialize your server project with this command first.**
**DO NOT manually create configuration files** like `package.json`, `tsconfig.json`, or `server.ts`. These files must be generated by the init command.
If the `server/` directory doesn't exist in your project, run:
```bash
npx -y @agent8/gameserver-node init
```
**This command is MANDATORY before writing any server code.** It automatically generates a complete server structure:
- `server/src/server.ts` - Your main server logic
- `server/test/server.test.ts` - Test files
- `server/package.json` - Dependencies and configuration (DO NOT create manually)
- `server/tsconfig.json` - TypeScript configuration (DO NOT create manually)
After initialization, you can modify the generated files to implement your game logic. The workflow is:
1. **First:** Run `init` command
2. **Then:** Modify generated files
3. **Never:** Skip init and create files manually
## TypeScript Server Implementation
The `server/src/server.ts` file uses TypeScript and requires an `export` statement:
```typescript filename="server/src/server.ts"
export class Server {
async ping(): Promise<string> {
return 'pong';
}
async getMyAccount(): Promise<string> {
return $sender.account;
}
async updateScore(points: number): Promise<number> {
const myState = await $global.getMyState();
const newScore = (myState.score || 0) + points;
await $global.updateMyState({ score: newScore });
return newScore;
}
}
```
The functionality is identical to `server.js`, but uses TypeScript with type safety.
## Testing
Write tests in `server/test/server.test.ts`:
```typescript filename="server/test/server.test.ts"
describe('Server', () => {
test('ping returns pong', async (server) => {
const result = await server.ping();
expect(result).toBe('pong');
});
test('connect changes user', async (server) => {
server.connect({ account: 'user-alice' });
const account = await server.getMyAccount();
expect(account).toBe('user-alice');
});
});
```
Run tests from your project directory:
```bash
npx -y @agent8/gameserver-node test
```
## Handler Namespaces
For better code organization, separate functionality into handlers using the `@Handler` decorator:
```typescript filename="server/src/userHandler.ts"
@Handler('user')
class UserHandler {
async getMyAccount(): Promise<string> {
return $sender.account;
}
}
```
Access namespaced functions:
**In tests:**
```typescript
const account = await server.user.getMyAccount();
```
**From client:**
```typescript
const result = await server.remoteFunction('user.getMyAccount');
```
## Building and Deployment
**Building and deployment are automatically handled by the Agent8 platform.** You don't need to manually run build or deploy commands.
When you push your code to the repository or use the platform's deployment features:
1. The platform automatically builds your TypeScript server from `server/src/` to `server/dist/server.js`
2. The platform automatically deploys the built server
**Important**: If `./server.js` exists in the project root, it takes priority over `server/dist/server.js` during deployment.
## Isolated VM Environment Limitations
Your server code runs in an **isolated-vm** environment for security and performance:
### ✅ Available
- Provided contexts: `$sender`, `$global`, `$room`, `$asset`
- Pure JavaScript/TypeScript libraries (lodash, date-fns, etc.)
- Most computation and logic processing
### ❌ Not Available
- Node.js built-in modules: `fs`, `http`, `https`, `net`, `child_process`, etc.
- Network request libraries: `axios`, `node-fetch`, etc.
- File system access
- External process execution
## Structured Project Workflow
1. **Initialize:** `npx -y @agent8/gameserver-node init` (required if `server/` directory doesn't exist)
2. **Develop:** Write server logic in `server/src/`
3. **Test:** `npx -y @agent8/gameserver-node test`
4. **Build:** `npx -y @agent8/gameserver-node build` (generates server/dist/server.js)
5. **Deploy:** Push to repository - building and deployment are handled automatically by the platform
# Migrating from Legacy server.js to Structured Project
If you have an existing `server.js` file and want to migrate to the Structured Project approach, follow these steps:
## Migration Steps
1. **Initialize the structured project:**
```bash
npx -y @agent8/gameserver-node init
```
2. **Convert to TypeScript:**
- Move your code from `server.js` to `server/src/server.ts`
- Add `export` to your Server class: `export class Server { ... }`
- Add TypeScript type annotations to function parameters and return types
- Convert JavaScript syntax to TypeScript where needed
3. **File Organization:**
- **If your `server.js` is large (>200 lines)**, consider splitting it into multiple files
- You can organize code in several ways:
**Option A: Keep everything in Server class** (no client changes needed)
```
server/src/
server.ts (main server class with all functions)
utils/
calculations.ts (helper functions - not exposed to clients)
```
- Client code remains the same: `remoteFunction('hello')`
**Option B: Use handler namespaces with `@Handler` decorator** (requires client updates)
```
server/src/
server.ts (main server class)
gameHandler.ts (@Handler('game') for game logic)
userHandler.ts (@Handler('user') for user management)
utils/
calculations.ts (helper functions)
```
- **Important:** Using handlers requires updating client code
- Example: `remoteFunction('hello')` becomes `remoteFunction('game.hello')`
- Choose this only if you want to organize functions into namespaces
4. **Migration Example:**
**Before (server.js):**
```js
class Server {
async updateScore(points) {
const myState = await $global.getMyState();
const newScore = (myState.score || 0) + points;
await $global.updateMyState({ score: newScore });
return newScore;
}
}
```
**After (server/src/server.ts):**
```typescript
export class Server {
async updateScore(points: number): Promise<number> {
const myState = await $global.getMyState();
const newScore = (myState.score || 0) + points;
await $global.updateMyState({ score: newScore });
return newScore;
}
}
```
5. **Write Tests:**
- Create tests in `server/test/server.test.ts`
- Test all migrated functions
- Run `npx -y @agent8/gameserver-node test` to verify
6. **Deploy:**
- Once tests pass, push to repository
- The platform will automatically build and deploy
**IMPORTANT:** After successful migration and deployment, you can delete the old `server.js` file from the project root.
# remoteFunction
remoteFunction can be called through the GameServer instance when connected.
```
const { connected, server } = useGameServer();
if (connected) server.remoteFunction(...);
```
Three ways to call remoteFunction:
1. Call requiring return value
Use `await remoteFunction('add', [1, 2])` when you need to wait for the server's computation result.
Note: Very rapid calls (several per second) may be rejected.
2. Non-response required but reliability guaranteed call (optional)
`remoteFunction('updateMyNickname', [{nick: 'karl'}], { needResponse: false })`
The needResponse option tells the server not to send a response, which can improve communication performance.
Rapid calls may still be rejected.
3. Overwritable update calls (fast call)
`remoteFunction('updateMyPosition', [{x, y}], { throttle: 50 })`
The throttle option sends requests at specified intervals (ms), ignoring intermediate calls.
Use this for fast real-time updates like player positions. The server only receives throttled requests, effectively reducing load.
3-1. Throttling multiple values simultaneously
`remoteFunction('updateBall', [{ballId: 1, position: {x, y}}], { throttle: 50, throttleKey: '1'})`
`remoteFunction('updateBall', [{ballId: 2, position: {x, y}}], { throttle: 50, throttleKey: '2'})`
Use throttleKey to differentiate throttling targets when updating multiple entities with the same function.
Without throttleKey, function name is used as default throttle identifier.
# Users
Users making server requests have a unique `account` ID:
- Access via `$sender.account` in server code
- Access via `server.account` in client code
```typescript filename='server/src/server.ts'
export class Server {
getMyAccount(): string {
return $sender.account;
}
}
```
```tsx filename='App.tsx'
const { connected, server } = useGameServer();
const myAccount = server.account;
```
# User Authentication & Premium Features
Users can have subscription and follower status in their authentication context. These properties enable premium features and follower-exclusive content:
- Access via `$sender.isFollower`, `$sender.isSubscriber`, and `$sender.subscription` in server code
- `$sender.isFollower` - boolean indicating if user follows the creator
- `$sender.isSubscriber` - boolean indicating if user has active subscription (checks expiration)
- `$sender.subscription` is `{ tier: string, exp: number } | null` (default tier: 'basic', expandable)
- Only available from server-signed authentication tokens (security feature)
```typescript filename='server/src/server.ts'
export class Server {
getMyStatus(): { isFollower: boolean; isSubscriber: boolean; subscription: { tier: string; exp: number } | null } {
return {
isFollower: $sender.isFollower,
isSubscriber: $sender.isSubscriber,
subscription: $sender.subscription
};
}
}
// Returns:
// {
// isFollower: true | false,
// isSubscriber: true | false,
// subscription: { tier: 'basic', exp: 1234567890 } | null
// }
```
# Global State Management
Multiplayer games require management of user states, items, rankings, etc.
Access global state through the `$global` variable in server code.
Agent8 provides three types of persistent global state:
1. Global Shared State
- `$global.getGlobalState(): Promise<Object>` - Retrieves current global state
- `$global.updateGlobalState(state: Object): Promise<Object>` - Updates global state
```typescript filename='server/src/server.ts'
export class Server {
async getGlobalState(): Promise<any> {
return $global.getGlobalState();
}
async updateGlobalState(state: any): Promise<void> {
await $global.updateGlobalState(state);
}
}
```
2. User State
- `$sender.account: string` - Request sender's account
- `$global.getUserState(account: string): Promise<Object>` - Gets specific user's state
- `$global.updateUserState(account: string, state: Object): Promise<Object>` - Updates specific user's state
- `$global.getMyState(): Promise<Object>` - Alias for <code>$global.getUserState($sender.account)</code>
- `$global.updateMyState(state: Object): Promise<Object>` - Alias for <code>$global.updateUserState($sender.account, state)</code>
3. Collection State (List management for specific keys, rankings)
- `$global.countCollectionItems(collectionId: string, options?: CollectionOptions): Promise<number>` - Retrieves number of items
- `$global.getCollectionItems(collectionId: string, options?: CollectionOptions): Promise<Item[]>` - Retrieves filtered/sorted items
- `$global.getCollectionItem(collectionId: string, itemId: string): Promise<Item>` - Gets single item by ID
- `$global.addCollectionItem(collectionId: string, item: any): Promise<Item>` - Adds new item
- `$global.updateCollectionItem(collectionId: string, item: any): Promise<Item>` - Updates existing item
- `$global.deleteCollectionItem(collectionId: string, itemId: string): Promise<{ \_\_id: string }>` - Deletes item
- `$global.deleteCollection(collectionId: string): Promise<string>` - Deletes Collection
CollectionOptions uses Firebase-style filtering:
```
interface QueryFilter {
field: string;
operator:
| '<'
| '<='
| '=='
| '!='
| '>='
| '>'
| 'array-contains'
| 'in'
| 'not-in'
| 'array-contains-any';
value: any;
}
interface QueryOrder {
field: string;
direction: 'asc' | 'desc';
}
interface CollectionOptions {
filters?: QueryFilter[];
orderBy?: QueryOrder[];
limit?: number;
startAfter?: any;
endBefore?: any;
}
```
**IMPORTANT**: You cannot use both `filters` and `orderBy` together in CollectionOptions because Firebase requires an index for such composite queries. You must choose either filtering OR ordering on a single field, not both simultaneously.
Important: All state updates in Agent8 use merge semantics
```typescript filename='server/src/server.ts'
const state = await $global.getGlobalState(); // { "name": "kim" }
await $global.updateGlobalState({ age: 18 });
const newState = await $global.getGlobalState(); // { "name": "kim", "age": 18 }
```
# Rooms - Optimized for Real-Time Session Games
Rooms are optimized for creating real-time multiplayer games. While global states handle numerous users, tracking them all isn't ideal. Rooms allow real-time awareness of connected users in the same space and synchronize all user states. There's a maximum connection limit.
You can manage all rooms through $global. These functions require explicit roomId specification.
- `$global.countRooms(): Promise<number>` - Gets the total number of rooms that currently have active users.
- `$global.getAllRoomIds(): Promise<string[]>` - Returns IDs of all rooms that currently have active users.
- `$global.getAllRoomStates(): Promise<any[]>` - Returns an array of roomStates for all rooms with active users.
- `$global.getRoomUserAccounts(roomId: string): Promise<string[]>` - Gets an array of account strings for current users in a specific room.
- `$global.countRoomUsers(roomId: string): Promise<number>` - Gets the number of current users in a specific room.
- `$global.getRoomState(roomId: string): Promise<any>`
- `$global.updateRoomState(roomId: string, state: any): Promise<any>`
- `$global.getRoomUserState(roomId: string, account: string): Promise<any>`
- `$global.updateRoomUserState(roomId: string, account: string, state: any): Promise<any>`
To join/leave rooms, you must call these functions:
- `$global.joinRoom(roomId?: string): Promise<string>`: Joins the specified room. If no roomId is provided, the server will create a new random room and return its ID.
- `$global.leaveRoom(): Promise<string>`: Leaves the current room. You can call `$room.leave()` instead.
IMPORTANT: `joinRoom()` request without roomId will always create a new random room. If you want users to join the same room by default, use a default roomId as a parameter.
Example for joining the room
```typescript filename='server/src/server.ts'
export class Server {
private MAX_ROOM_USER = 3;
async joinRoom(roomId?: string): Promise<string> {
if (roomId) {
if (await $global.countRoomUsers(roomId) >= this.MAX_ROOM_USER) {
throw new Error('room is full');
}
}
const joinedRoomId = await $global.joinRoom(roomId);
if (await $global.countRoomUsers(joinedRoomId) === this.MAX_ROOM_USER) {
// or you can use `await $room.countUsers() === this.MAX_ROOM_USER`
await $room.updateRoomState({ status: 'START' });
} else {
await $room.updateRoomState({ status: 'READY' });
}
return joinedRoomId;
}
}
```
The $room prefix automatically acts on the room that the $sender belongs to, so you don't need to explicitly specify roomId. All actions are queried and processed for the currently joined room.
Rooms provide a tick function that enables automatic server-side logic execution without explicit user calls.
The room tick will only repeat execution while users are present in the room.
The room tick is a function that is called every 100ms~1000ms (depends on the game's logic)
```typescript filename='server/src/server.ts'
export class Server {
$roomTick(deltaMS: number, roomId: string): void {
// ... your game logic here
}
}
```
Important: Do not use `setInterval` or `setTimeout` in server code - you must use room ticks instead.
Room state can be accessed through the `$room` public variable in server code. Agent8 provides three types of room states that are not persisted - they get cleared when all users leave the room.
1. Room Public State
- `$room.getRoomState(): Promise<Object>` - Retrieves the current room state.
- `$room.updateRoomState(state: Object): Promise<Object>` - Updates the room state with new values.
Important: roomState contains these default values:
- `roomId: string`
- `$users: string[]` - Array of all user accounts in the room, automatically updated
2. Room User State
- `$sender.roomId: string` - The ID of the room the sender is in.
- `$room.getUserState(account: string): Promise<Object>` — Retrieves a particular user's state in the room.
- `$room.updateUserState(account: string, state: Object): Promise<Object>` — Updates a particular user's state in the room.
- `$room.getMyState(): Promise<Object>` — Retrieves $sender state in the room.
- `$room.updateMyState(state: Object): Promise<Object>` — Updates $sender state in the room.
- `$room.getAllUserStates(): Promise<Object[]>` - Retrieves all user states in the room.
3. Room Collection State
- `$room.countCollectionItems(collectionId: string, options?: CollectionOptions): Promise<number>` - Retrieves number of items from a room collection based on filtering, sorting, and pagination options.
- `$room.getCollectionItems(collectionId: string, options?: CollectionOptions): Promise<Item[]>` - Retrieves multiple items from a room collection based on filtering, sorting, and pagination options.
- `$room.getCollectionItem(collectionId: string, itemId: string): Promise<Item>` - Retrieves a single item from the room collection using its unique ID.
- `$room.addCollectionItem(collectionId: string, item: any): Promise<Item>` - Adds a new item to the specified room collection and returns the added item with its unique identifier.
- `$room.updateCollectionItem(collectionId: string, item: any): Promise<Item>` - Updates an existing item in the room collection and returns the updated item.
- `$room.deleteCollectionItem(collectionId: string, itemId: string): Promise<{ \_\_id: string }>` - Deletes an item from the room collection and returns a confirmation containing the deleted item's ID.
- `$room.deleteCollection(collectionId: string): Promise<string>` - Delete room collection.
# Messaging
Simple socket messaging is also supported:
1. Global Messages
- `$global.broadcastToAll(type: string, message: any)` - Broadcasts a message to all connected users globally.
- `$global.sendMessageToUser(type: string, account: string, message: any)` - Sends a direct message to a specific user account globally.
2. Room Messages
- `$room.broadcastToRoom(type: string, message: any)` - Broadcasts a message to all users in the current room.
- `$room.sendMessageToUser(type: string, account: string, message: any)` - Sends a direct message to a specific user within the current room.
Very Important: $global, $room, and $sender are all used in server code (`server/src/server.ts` for Structured Projects).
# Subscribing to State Changes on Client
The `server` object from `const { server } = useGameServer()` contains these subscription functions:
1. Global State Subscriptions
- `server.subscribeGlobalState((state: any) => {}): UnsubscribeFunction`
- `server.subscribeGlobalUserState(account, (state: any) => {}): UnsubscribeFunction`
- `server.subscribeGlobalMyState((state: any) => {}): UnsubscribeFunction`
- `server.subscribeGlobalCollection(collectionId, ({ items, changes } : { items: any[], changes: { op: 'add' | 'update' | 'delete' | 'deleteAll', items: any[]}}) => {}): UnsubscribeFunction`
2. Room State Subscriptions
- `server.subscribeRoomState(roomId, (state: {...state: any, $users: string[]}) => {}): UnsubscribeFunction`
- `server.subscribeRoomUserState(roomId, account, (state: any) => {}): UnsubscribeFunction`
- `server.subscribeRoomAllUserStates(roomId, (states: { ...state: any, account: string, updated: boolean }[]) => {}): UnsubscribeFunction` - All user states, The changed state, which is the cause of the subscription, is set to `updated: true`.
- `server.subscribeRoomMyState(roomId, (state: any) => {}): UnsubscribeFunction`
- `server.subscribeRoomCollection(roomId, collectionId, ({ items, changes} : { items: any[], changes: {op: 'add' | 'update' | 'delete' | 'deleteAll', items: any[]}}) => {}): UnsubscribeFunction`
- `server.onRoomUserJoin(roomId, (account: string) => {}): UnsubscribeFunction` - Triggered when a user joins the room.
- `server.onRoomUserLeave(roomId, (account: string) => {}): UnsubscribeFunction` - Triggered when a user leaves the room.
3. Message Receiving
- `server.onGlobalMessage(type: string, (message: any) => {})`
- `server.onRoomMessage(roomId: string, type: string, (message: any) => {})`
IMPORTANT: All subscribe functions are triggered when the state is updated. When subscribing, the current value is received once.
# Real-time State with React Hooks
In environments supporting React hooks, you can get real-time server state updates on the client without explicit subscriptions.
- Getting global state:
```tsx filename='App.tsx'
import {
useGlobalState,
useGlobalMyState,
useGlobalUserState,
useGlobalCollection,
} from "@agent8/gameserver";
const globalState = useGlobalState();
const myState = useGlobalMyState();
const userState = useGlobalUserState(account); // Specific user's state
const { items } = useGlobalCollection(collectionId); // Collection
```
- Getting room state (roomId is automatically handled in hooks):
```tsx filename='App.tsx'
import {
useRoomState,
useRoomMyState,
useRoomUserState,
useRoomAllUserStates,
useRoomCollection,
} from "@agent8/gameserver";
const roomState = useRoomState(); // Room public state
const roomMyState = useRoomMyState(); // my state
const userState = useRoomUserState(account); // Specific user's state
const states = useRoomAllUserStates(); // all users states in the room ({ ...state:any, account: string }[])
const { items } = useRoomCollection(collectionId); // Collection
```
## Asset Management
Agent8 provides comprehensive asset management for in-game currencies and fungible resources. All operations are automatically validated and synchronized:
### Server-side Asset API ($asset)
- `$asset.get(assetId: string): Promise<number>` - Get asset amount
- `$asset.getAll(): Promise<Record<string, number>>` - Get all assets
- `$asset.has(assetId: string, amount: number): Promise<boolean>` - Check asset availability
- `$asset.mint(assetId: string, amount: number): Promise<Record<string, number>>` - Create assets
- `$asset.burn(assetId: string, amount: number): Promise<Record<string, number>>` - Destroy assets
- `$asset.transfer(toAccount: string, assetId: string, amount: number): Promise<Record<string, number>>` - Transfer assets
**IMPORTANT**: Never expose mint/burn directly to clients. Always wrap in server-controlled logic:
```typescript filename='server/src/server.ts'
export class Server {
// Server determines reward amount - client only sends missionId
async completeMission(missionId: string): Promise<Record<string, number>> {
const rewards: Record<string, number> = { 'daily': 100, 'boss': 500 };
const reward = rewards[missionId];
if (!reward) throw new Error('Invalid mission');
return await $asset.mint('gold', reward);
}
// Server defines prices - client only sends itemId
async buyItem(itemId: string): Promise<Record<string, number>> {
const prices: Record<string, number> = { 'sword': 100, 'potion': 25 };
const price = prices[itemId];
if (!price) throw new Error('Item not found');
if (!await $asset.has('gold', price)) throw new Error('Insufficient gold');
await $asset.burn('gold', price);
return await $asset.mint(itemId, 1);
}
}
```
### Client-side Asset Hook (useAsset)
The `useAsset` hook provides **read-only** access to the current user's assets. All modifications must be done server-side.
```tsx
import { useAsset } from "@agent8/gameserver";
const { assets, getAsset, hasAsset } = useAsset();
// Check if user can afford something
if (hasAsset('gold', 100)) {
// Call server function to perform the purchase
await server.remoteFunction('buyItem', ['sword']);
}
```
## System Event Handlers
Special handlers called automatically by the system. Protected from client calls (`$on` prefix).
### $onItemPurchased - VX Shop Purchase Handler
Called when a user purchases an item from VX Shop. Use to deliver purchased items.
- `account: string` - User's wallet address
- `purchaseId: number` - Unique purchase ID
- `productId: string` - Product identifier
- `quantity: number` - Number purchased
- `metadata?: object` - Optional data
```typescript filename='server/src/server.ts'
export class Server {
async $onItemPurchased({
account,
purchaseId,
productId,
quantity
}: {
account: string;
purchaseId: number;
productId: string;
quantity: number;
}): Promise<{ success: boolean }> {
// Grant items by productId
if (productId === 'gold_pack') {
await $asset.mint('gold', 100 * quantity);
} else if (productId === 'premium_skin') {
const user = await $global.getMyState();
await $global.updateMyState({ skins: [...(user.skins || []), productId] });
} else {
await $asset.mint(productId, quantity);
}
return { success: true };
}
}
```
ULTRA IMPORTANT: Does not support `setInterval` or `setTimeout` in server code. NEVER use them.
ULTRA IMPORTANT: Project Initialization:
- **ALWAYS run `npx -y @agent8/gameserver-node init` FIRST** if `server/` directory doesn't exist
- **NEVER manually create** `server/package.json`, `server/tsconfig.json`, or `server/src/server.ts` without running init
- **DO NOT skip the init command** - it generates all necessary configuration files correctly
- Only modify files AFTER init completes successfully
ULTRA IMPORTANT: Export syntax differences:
- **Structured Project (`server/src/server.ts`)**: MUST use `export class Server` - the export statement is REQUIRED
- **Legacy (`server.js`)**: MUST NOT use any export statement - just define `class Server` without export
ULTRA IMPORTANT: File locations:
- **Structured Projects**: server code must be in `server/src/server.ts` and built to `server/dist/server.js`
- **Legacy projects**: `server.js` must be placed in the project root <boltAction type="file" filePath="server.js">Last updated on