### Launching Endpoint Source: https://context7.com/nhn/gameanvil.sample-game-server/llms.txt Provides a RESTful HTTP GET endpoint for clients to retrieve game server information for bootstrapping. ```APIDOC ## REST Launching Endpoint — `LaunchingSupport` / `_Launching` A `BaseSupportNode` that exposes a single HTTP GET endpoint for pre-game bootstrapping. Returns the game server's IP and port to connecting clients. ```java // Node registration (LaunchingSupport.java): // restMsgHandler.registerMsg("/launching", RestObject.GET, _Launching.class); // Handler (_Launching.java): public void execute(LaunchingSupport target, RestObject restObject) throws SuspendExecution { Map> params = restObject.getRequestParameters(); String platform = params.get("platform").get(0).toUpperCase(); // e.g. "IOS" String appStore = params.get("appStore").get(0).toUpperCase(); // e.g. "APPLE" String appVersion = params.get("appVersion").get(0); // e.g. "1.0.0" String deviceId = params.get("deviceId").get(0); // e.g. "device-xyz" JsonObject response = new JsonObject(); if (platform != null && appStore != null && appVersion != null && deviceId != null) { response.addProperty("serverUrl", "127.0.0.1"); response.addProperty("port", 18200); response.addProperty("status", 200); response.addProperty("message", "SUCCESS"); } else { response.addProperty("status", 400); response.addProperty("message", "Invalid Parameters: platform, appStore, appVersion, deviceId"); } restObject.writeString(GameAnvilUtil.Gson().toJson(response)); } // curl usage: // curl "http://127.0.0.1:18600/launching?platform=Android&appStore=Google&appVersion=1.0.0&deviceId=abc123" // → {"serverUrl":"127.0.0.1","port":18200,"status":200,"message":"SUCCESS"} ``` ``` -------------------------------- ### Server Bootstrap - Main Class Source: https://context7.com/nhn/gameanvil.sample-game-server/llms.txt The entry point for the GameAnvil server. It registers protobuf classes, creates thread pools, scans for GameAnvil annotations, and starts the server. Ensure JVM arguments are set for memory and garbage collection. ```java public class Main { public static void main(String[] args) { GameAnvilServer gameAnvilServer = GameAnvilServer.getInstance(); // Register protobuf protocol classes — order must match the client gameAnvilServer.addProtoBufClass(Authentication.class); gameAnvilServer.addProtoBufClass(GameMulti.class); gameAnvilServer.addProtoBufClass(GameSingle.class); gameAnvilServer.addProtoBufClass(Result.class); gameAnvilServer.addProtoBufClass(User.class); // Named thread pools for off-fiber blocking I/O gameAnvilServer.createExecutorService(GameConstants.DB_THREAD_POOL, 100); gameAnvilServer.createExecutorService(GameConstants.REDIS_THREAD_POOL, 100); // Scan for @GatewayNode, @Connection, @Session, @ServiceName, @RoomType, etc. gameAnvilServer.addPackageToScan("com.nhn.gameanvil.sample"); gameAnvilServer.run(); // blocks until shutdown } } // Build and run via Gradle: // ./gradlew runMain // JVM args applied automatically: -Xms6g -Xmx6g -XX:+UseG1GC ``` -------------------------------- ### REST Launching Endpoint Handler (Java) Source: https://context7.com/nhn/gameanvil.sample-game-server/llms.txt Implement a REST endpoint using LaunchingSupport to provide game server IP and port to clients. Handles GET requests with platform, appStore, appVersion, and deviceId parameters. ```java // Node registration (LaunchingSupport.java): // restMsgHandler.registerMsg("/launching", RestObject.GET, _Launching.class); // Handler (_Launching.java): public void execute(LaunchingSupport target, RestObject restObject) throws SuspendExecution { Map> params = restObject.getRequestParameters(); String platform = params.get("platform").get(0).toUpperCase(); // e.g. "IOS" String appStore = params.get("appStore").get(0).toUpperCase(); // e.g. "APPLE" String appVersion = params.get("appVersion").get(0); // e.g. "1.0.0" String deviceId = params.get("deviceId").get(0); // e.g. "device-xyz" JsonObject response = new JsonObject(); if (platform != null && appStore != null && appVersion != null && deviceId != null) { response.addProperty("serverUrl", "127.0.0.1"); response.addProperty("port", 18200); response.addProperty("status", 200); response.addProperty("message", "SUCCESS"); } else { response.addProperty("status", 400); response.addProperty("message", "Invalid Parameters: platform, appStore, appVersion, deviceId"); } restObject.writeString(GameAnvilUtil.Gson().toJson(response)); } // curl usage: // curl "http://127.0.0.1:18600/launching?platform=Android&appStore=Google&appVersion=1.0.0&deviceId=abc123" // → {"serverUrl":"127.0.0.1","port":18200,"status":200,"message":"SUCCESS"} ``` -------------------------------- ### Snake Room Implementation Source: https://context7.com/nhn/gameanvil.sample-game-server/llms.txt Manages a 2-player Snake game room. Handles player joins, game start, food spawning, and player leaves. ```java @ServiceName(GameConstants.GAME_NAME) @RoomType(GameConstants.GAME_ROOM_TYPE_MULTI_USER_MATCH) public class SnakeRoom extends BaseRoom { private static final int FOOD_MAX_COUNT = 200; private int boarderLeft = -35, boarderRight = 35; private int boarderBottom = -25, boarderTop = 25; @Override public boolean onJoinRoom(GameUser gameUser, Payload inPayload, Payload outPayload) throws SuspendExecution { gameUser.getSnakePositionInfoList().clear(); // Second player starts at bottom-left gameUser.getSnakePositionInfoList().add( new SnakePositionInfo(1, boarderLeft + 5, boarderBottom + 5)); gameUserMap.put(gameUser.getUserId(), gameUser); gameUserScoreMap.put(gameUser.getGameUserInfo().getUuid(), 0); if (gameUserMap.size() == 2) { // Both players present: broadcast initial game state and start food timer for (GameUser u : gameUserMap.values()) { u.send(getSnakeGameInfoMsgByProto()); } addTimer(1, TimeUnit.SECONDS, 0, "SnakeGameTimerHandler", timerHandler, false); } outPayload.add(gameUser.getRoomInfoMsgByProto(RoomGameType.ROOM_SNAKE)); return true; } @Override public void onLeaveRoom(GameUser gameUser) throws SuspendExecution { gameUserMap.remove(gameUser.getUserId()); removeAllTimer(); // stop food spawner for (GameUser remaining : gameUserMap.values()) { remaining.kickoutRoom(); // end game for remaining player } } } ``` -------------------------------- ### RedisHelper - Get Single Ranking Source: https://context7.com/nhn/gameanvil.sample-game-server/llms.txt Fetches a range of scores from the single-player ranking sorted set. ```APIDOC ## GET RedisHelper.getSingleRanking ### Description Fetches a paginated list of scores from the "taptap_single_score" sorted set in descending order. ### Method GET ### Endpoint /RedisHelper/getSingleRanking ### Parameters #### Query Parameters - **start** (int) - Required - The starting index of the range (0-indexed). - **end** (int) - Required - The ending index of the range (0-indexed). ### Response #### Success Response (200) - **rankings** (Map) - A map where keys are user UUIDs and values contain UUID and score. ### Response Example ```json { "rankings": { "uuid1": {"uuid": "uuid1", "score": 4200.0}, "uuid2": {"uuid": "uuid2", "score": 3100.0} } } ``` ``` -------------------------------- ### Single-Player Game Room Logic Source: https://context7.com/nhn/gameanvil.sample-game-server/llms.txt Manages a single-player game session. Deducts hearts on room creation and updates high scores in MySQL and Redis on room leave. Requires sufficient hearts to start. ```java @ServiceName(GameConstants.GAME_NAME) @RoomType(GameConstants.GAME_ROOM_TYPE_SINGLE) // "SingleTapRoom" public class SingleGameRoom extends BaseRoom { // Registered handler: GameSingle.TapMsg → _TapMsg (increments score server-side) static { dispatcher.registerMsg(GameSingle.TapMsg.class, _TapMsg.class); } @Override public boolean onCreateRoom(GameUser gameUser, Payload inPayload, Payload outPayload) throws SuspendExecution { GameSingle.StartGameReq req = inPayload.getProtoBuffer(GameSingle.StartGameReq.getDescriptor()); if (gameUser.getGameUserInfo().getHeart() < 1) { // Fail: not enough hearts return false; } gameUser.getGameUserInfo().useHeart(); // deduct 1 heart singleGameData.setDeck(req.getDeck()); // e.g. "sushi" singleGameData.setDifficulty(req.getDifficultyValue()); GameSingle.StartGameRes.Builder res = GameSingle.StartGameRes.newBuilder(); res.setErrorCode(ErrorCode.NONE); outPayload.add(res); return true; } @Override public boolean onLeaveRoom(GameUser gameUser, Payload inPayload, Payload outPayload) throws SuspendExecution { // Persist new high score if applicable if (gameUser.getGameUserInfo().getHighScore() < singleGameData.getScore()) { int dbCount = ((GameNode) getBaseGameNode()).getJAsyncSqlManager() .updateUserHigScore(gameUser.getGameUserInfo().getUuid(), singleGameData.getScore()); if (dbCount == 1) { ((GameNode) GameNode.getInstance()).getRedisHelper() .setSingleScore(gameUser.getGameUserInfo().getUuid(), singleGameData.getScore()); } } GameSingle.EndGameRes.Builder endRes = GameSingle.EndGameRes.newBuilder(); endRes.setErrorCode(ErrorCode.NONE); endRes.setUserData(gameUser.getUserDataByProto()); endRes.setTotalScore(singleGameData.getTotalScore()); outPayload.add(endRes); return true; } } ``` -------------------------------- ### User Login - GameUser.onLogin() Source: https://context7.com/nhn/gameanvil.sample-game-server/llms.txt Processes the LoginReq protobuf packet on first login. It handles user lookup in MySQL, creates new records if necessary, caches user data in Redis, and returns LoginRes with user data. ```APIDOC ## GameUser.onLogin() ### Description Processes the `LoginReq` protobuf packet on first login: looks up the user in MySQL (via JAsyncSQL or MyBatis), creates a new record with default stats if not found, caches the result to Redis, and returns `LoginRes` with full `UserData`. ### Method ```java @Override public boolean onLogin(Payload payload, Payload accountPayload, Payload outPayload) throws SuspendExecution ``` ### Request Body - **payload** (Payload) - Contains the `LoginReq` protobuf packet. - **accountPayload** (Payload) - Account-related payload. - **outPayload** (Payload) - Payload to be populated with the response. ### Response #### Success Response - **outPayload** will contain `LoginRes` which includes `ErrorCode` and `UserData`. ### Request Example ```java // Protocol: Authentication.proto // message LoginReq { string uuid=1; LoginType loginType=2; string appVersion=3; ... } // message LoginRes { ErrorCode errorCode=1; UserData userdata=2; } Authentication.LoginReq loginReq = payload.getPacket(Authentication.LoginReq.getDescriptor()).toProtoBufferMessage(); // ... process loginReq ... Authentication.LoginRes.Builder res = Authentication.LoginRes.newBuilder(); res.setErrorCode(ErrorCode.NONE); res.setUserdata(getUserDataByProto()); outPayload.add(res); ``` ``` -------------------------------- ### Process User Login - GameUser.onLogin() Source: https://context7.com/nhn/gameanvil.sample-game-server/llms.txt Handles user login by looking up user data in MySQL, creating a new record if necessary, caching to Redis, and returning user data. Requires Authentication.proto definitions. ```java // Protocol: Authentication.proto // message LoginReq { string uuid=1; LoginType loginType=2; string appVersion=3; ... } // message LoginRes { ErrorCode errorCode=1; UserData userdata=2; } @ServiceName(GameConstants.GAME_NAME) @UserType(GameConstants.GAME_USER_TYPE) // "TapTapUser" public class GameUser extends BaseUser { @Override public boolean onLogin(Payload payload, Payload accountPayload, Payload outPayload) throws SuspendExecution { Authentication.LoginReq loginReq = payload.getPacket(Authentication.LoginReq.getDescriptor()).toProtoBufferMessage(); gameUserInfo.setUuid(loginReq.getUuid()); // ... set device fields ... // DB lookup (JAsyncSQL path) GameUserInfo dbInfo = ((GameNode) getBaseGameNode()) .getJAsyncSqlManager().selectUserByUuid(gameUserInfo.getUuid()); if (dbInfo == null) { // New user: set defaults and persist gameUserInfo.setNickname("GameAnvil-" + getUserId()); gameUserInfo.setHeart(3); gameUserInfo.setCoin(10000); gameUserInfo.setRuby(100); gameUserInfo.setLevel(1); gameUserInfo.setHighScore(0); gameUserInfo.setCurrentDeck("sushi"); ((GameNode) getBaseGameNode()).getJAsyncSqlManager().insertUser(gameUserInfo); } else { // Existing user: restore from DB gameUserInfo.setNickname(dbInfo.getNickname()); gameUserInfo.setCoin(dbInfo.getCoin()); // ... restore other fields ... } // Cache to Redis for ranking cross-reference ((GameNode) GameNode.getInstance()).getRedisHelper().setUserData(gameUserInfo); Authentication.LoginRes.Builder res = Authentication.LoginRes.newBuilder(); res.setErrorCode(ErrorCode.NONE); res.setUserdata(getUserDataByProto()); outPayload.add(res); return true; } } ``` -------------------------------- ### Game Node Initialization - GameNode Source: https://context7.com/nhn/gameanvil.sample-game-server/llms.txt Initializes Redis and JAsyncSQL connections for each game node instance. ```APIDOC ## Game Node Initialization — `GameNode` ### Description The `@ServiceName`-annotated `GameNode` initializes one Redis cluster connection and one JAsyncSQL connection pool per node instance. Connections are **not** singletons — each node gets its own clients to avoid cross-fiber contention. ### Methods - **`onInit()`** - Called during node initialization. - Establishes connections to Redis and the database. - **`onShutdown()`** - Called during node shutdown. - Closes the established connections. ### Initialization Details - **Redis Connection**: Uses `RedisHelper` to connect to a specified URL and port. - **Database Connection**: Uses `JAsyncSqlManager` with provided credentials and configuration. ### Example Usage ```java @ServiceName(GameConstants.GAME_NAME) // "TapTap" public class GameNode extends BaseGameNode { private RedisHelper redisHelper; private JAsyncSqlManager jAsyncSqlManager; @Override public void onInit() throws SuspendExecution { // Each game node creates its own Redis and DB connections redisHelper = new RedisHelper(); redisHelper.connect(GameConstants.REDIS_URL, GameConstants.REDIS_PORT); // e.g. REDIS_URL="192.168.0.10", REDIS_PORT=6379 jAsyncSqlManager = new JAsyncSqlManager( GameConstants.DB_USERNAME, // "taptap" GameConstants.DB_HOST, // "127.0.0.1" GameConstants.DB_PORT, // 3306 GameConstants.DB_PASSWORD, GameConstants.DB_DATABASE, // "taptap" GameConstants.MAX_ACTIVE_CONNECTION // 30 ); } @Override public void onShutdown() throws SuspendExecution { redisHelper.shutdown(); jAsyncSqlManager.close(); } } ``` ``` -------------------------------- ### Initialize Game Node Connections in GameNode Source: https://context7.com/nhn/gameanvil.sample-game-server/llms.txt Initialize Redis and JAsyncSQL connections within the `GameNode`'s `onInit` method. Each node instance creates its own connections to avoid cross-fiber contention. ```java @ServiceName(GameConstants.GAME_NAME) // "TapTap" public class GameNode extends BaseGameNode { private RedisHelper redisHelper; private JAsyncSqlManager jAsyncSqlManager; @Override public void onInit() throws SuspendExecution { // Each game node creates its own Redis and DB connections redisHelper = new RedisHelper(); redisHelper.connect(GameConstants.REDIS_URL, GameConstants.REDIS_PORT); // e.g. REDIS_URL="192.168.0.10", REDIS_PORT=6379 jAsyncSqlManager = new JAsyncSqlManager( GameConstants.DB_USERNAME, // "taptap" GameConstants.DB_HOST, // "127.0.0.1" GameConstants.DB_PORT, // 3306 GameConstants.DB_PASSWORD, GameConstants.DB_DATABASE, // "taptap" GameConstants.MAX_ACTIVE_CONNECTION // 30 ); } @Override public void onShutdown() throws SuspendExecution { redisHelper.shutdown(); jAsyncSqlManager.close(); } } ``` -------------------------------- ### Room-Based Matchmaking - GameUser.onMatchRoom() Source: https://context7.com/nhn/gameanvil.sample-game-server/llms.txt Initiates room-based matchmaking by joining an available UnlimitedTapRoom. Returns RoomMatchResult.FAILED on error. ```java // Room-based match: client requests to join any open UnlimitedTapRoom @Override public final RoomMatchResult onMatchRoom(final String roomType, final String matchingGroup, final String matchingUserCategory, final Payload payload) throws SuspendExecution { UnlimitedTapRoomMatchForm form = new UnlimitedTapRoomMatchForm(); return matchRoom(matchingGroup, roomType, form); // Returns RoomMatchResult.FAILED on error → client either creates room or fails } ``` -------------------------------- ### Runtime JVM Flags and Configuration (Bash) Source: https://context7.com/nhn/gameanvil.sample-game-server/llms.txt Specify required JVM flags and program arguments for GameAnvil applications, including Quasar JIT instrumentation agent and illegal access settings for JDK 11. ```bash # JVM flags required at runtime (setting.txt) -Dco.paralleluniverse.fibers.detectRunawayFibers=false -Dco.paralleluniverse.fibers.verifyInstrumentation=false -Xms4g -Xmx4g -XX:+UseG1GC -XX:MaxGCPauseMillis=100 # JDK 11 extra --add-opens=java.base/java.lang=ALL-UNNAMED --illegal-access=deny # Quasar JIT instrumentation agent (JDK 11) -javaagent:./src/main/resources/META-INF/quasar-core-0.7.10-jdk11.jar=bm # Program argument: resource directory path src/main/resources/ ``` -------------------------------- ### Handle User Re-Login - GameUser.onReLogin() Source: https://context7.com/nhn/gameanvil.sample-game-server/llms.txt Returns the current in-memory user state immediately for re-connecting users without a full login flow. ```java @Override public boolean onReLogin(Payload payload, Payload accountPayload, Payload outPayload) throws SuspendExecution { Authentication.LoginRes.Builder loginRes = Authentication.LoginRes.newBuilder(); loginRes.setUserdata(getUserDataByProto()); // returns current in-memory state outPayload.add(loginRes); return true; } ``` -------------------------------- ### User Matchmaking - GameUser.onMatchRoom() and GameUser.onMatchUser() Source: https://context7.com/nhn/gameanvil.sample-game-server/llms.txt Supports two matchmaking modes: room-based matching (joining available rooms) and user-based matching (pairing two players). ```APIDOC ## GameUser.onMatchRoom() and GameUser.onMatchUser() ### Description Two matchmaking modes are supported. `onMatchRoom()` uses **room-based matching** (join any available UnlimitedTapRoom), while `onMatchUser()` uses **user-based matching** (pair two players into a new SnakeRoom by rating bracket). ### Methods #### `onMatchRoom()` - **Method Signature:** ```java @Override public final RoomMatchResult onMatchRoom(final String roomType, final String matchingGroup, final String matchingUserCategory, final Payload payload) throws SuspendExecution ``` - **Description:** Joins an available room based on specified criteria. - **Parameters:** - **roomType** (String) - The type of room to join. - **matchingGroup** (String) - The matching group identifier. - **matchingUserCategory** (String) - The category of the user for matching. - **payload** (Payload) - Additional payload data. - **Returns:** `RoomMatchResult` (e.g., `RoomMatchResult.FAILED` on error). #### `onMatchUser()` - **Method Signature:** ```java @Override public boolean onMatchUser(final String roomType, final String matchingGroup, final Payload payload, Payload outPayload) throws SuspendExecution ``` - **Description:** Pairs two players into a new room based on their rating bracket. - **Parameters:** - **roomType** (String) - The type of room to create. - **matchingGroup** (String) - The matching group identifier. - **payload** (Payload) - Additional payload data. - **outPayload** (Payload) - Payload to be populated with the response. - **Returns:** `boolean` (true if match request queued successfully). ### Request Example (onMatchRoom) ```java UnlimitedTapRoomMatchForm form = new UnlimitedTapRoomMatchForm(); return matchRoom(matchingGroup, roomType, form); ``` ### Request Example (onMatchUser) ```java // rating=100 places user in the 100-199 rating bracket SnakeUserMatchInfo term = new SnakeUserMatchInfo(getUserId(), 100); return matchUser(matchingGroup, roomType, term); ``` ``` -------------------------------- ### RedisHelper - Connect Source: https://context7.com/nhn/gameanvil.sample-game-server/llms.txt Establishes a connection to the Redis cluster. ```APIDOC ## POST RedisHelper.connect ### Description Connects to the Redis cluster using the provided URL and port. ### Method POST ### Endpoint /RedisHelper/connect ### Parameters #### Request Body - **url** (string) - Required - The URL of the Redis server. - **port** (int) - Required - The port of the Redis server. - **password** (string) - Optional - The password for Redis authentication. ``` -------------------------------- ### User-Based Matchmaking - GameUser.onMatchUser() Source: https://context7.com/nhn/gameanvil.sample-game-server/llms.txt Initiates user-based matchmaking to pair two players into a new SnakeRoom based on rating brackets. Returns true if the match request is queued successfully. ```java // User-based match: client requests to be paired with another player @Override public boolean onMatchUser(final String roomType, final String matchingGroup, final Payload payload, Payload outPayload) throws SuspendExecution { // rating=100 places user in the 100-199 rating bracket SnakeUserMatchInfo term = new SnakeUserMatchInfo(getUserId(), 100); return matchUser(matchingGroup, roomType, term); // Returns true if match request queued successfully } ``` -------------------------------- ### Quasar Instrumentation in Gradle Build (Gradle) Source: https://context7.com/nhn/gameanvil.sample-game-server/llms.txt Configure Gradle to run Quasar fiber instrumentation as a post-compile step. Ensure the Quasar JAR is available in the API configuration. ```gradle // build.gradle — Quasar instrumentation runs after compileJava compileJava { dependsOn.processResources doLast { ant.taskdef(name: 'instrumentation', classname: 'co.paralleluniverse.fibers.instrument.InstrumentationTask', classpath: configurations.api.asPath) ant.instrumentation(verbose: 'true', check: 'true', debug: 'true') { fileset(dir: 'build/classes/') { include(name: '**/*.class') } } } } // Key dependencies dependencies { api files('./src/main/resources/META-INF/quasar-core-0.8.0-jdk11.jar') api 'org.mybatis:mybatis:3.5.3' api 'mysql:mysql-connector-java:8.0.23' api 'com.nhn.gameanvil:gameanvil:1.4.1-jdk11' } ``` -------------------------------- ### User Transfer - GameUser.onTransferOut() / GameUser.onTransferIn() Source: https://context7.com/nhn/gameanvil.sample-game-server/llms.txt Supports non-stop patching by serializing and restoring the user's game state during node migrations. ```APIDOC ## GameUser.onTransferOut() and GameUser.onTransferIn() ### Description Supports non-stop patching by serializing the user's game state into a `TransferPack` so it can be restored after a node migration. ### Methods #### `onTransferOut()` - **Method Signature:** ```java @Override public void onTransferOut(TransferPack transferPack) throws SuspendExecution ``` - **Description:** Serializes the user's current game state into a `TransferPack`. - **Parameters:** - **transferPack** (TransferPack) - The pack to which the user's state will be added. #### `onTransferIn()` - **Method Signature:** ```java @Override public void onTransferIn(TransferPack transferPack) throws SuspendExecution ``` - **Description:** Restores the user's game state from a `TransferPack`. - **Parameters:** - **transferPack** (TransferPack) - The pack from which to restore the user's state. ### Request Example (onTransferOut) ```java GameUserTransferInfo transferInfo = new GameUserTransferInfo(); transferInfo.setGameUserInfo(gameUserInfo); transferInfo.setGetSnakePositionInfoList(snakePositionInfoList); transferPack.put("GameUserInfo", transferInfo); ``` ### Request Example (onTransferIn) ```java GameUserTransferInfo transferInfo = (GameUserTransferInfo) transferPack.get("GameUserInfo"); gameUserInfo = transferInfo.getGameUserInfo(); snakePositionInfoList = transferInfo.getGetSnakePositionInfoList(); ``` ``` -------------------------------- ### Serialize User State for Transfer - GameUser.onTransferOut() Source: https://context7.com/nhn/gameanvil.sample-game-server/llms.txt Serializes the user's game state into a TransferPack for non-stop patching during node migration. ```java @Override public void onTransferOut(TransferPack transferPack) throws SuspendExecution { GameUserTransferInfo transferInfo = new GameUserTransferInfo(); transferInfo.setGameUserInfo(gameUserInfo); transferInfo.setGetSnakePositionInfoList(snakePositionInfoList); transferPack.put("GameUserInfo", transferInfo); } ``` -------------------------------- ### Connection Authentication - GameConnection.onAuthenticate() Source: https://context7.com/nhn/gameanvil.sample-game-server/llms.txt Handles client authentication by validating account ID, password, and access token. Returns true for successful authentication, false otherwise. ```APIDOC ## Connection Authentication — `GameConnection.onAuthenticate()` ### Description Called by the framework when a client attempts to authenticate. The implementation validates that `accountId` equals `password` (test logic) and checks that `AuthenticationReq.accessToken` starts with a known prefix. Returns `true` to allow, `false` to deny. ### Method Signature `boolean onAuthenticate(String accountId, String password, String deviceId, Payload payload, Payload outPayload)` ### Parameters - **accountId** (String) - The account ID provided by the client. - **password** (String) - The password provided by the client. - **deviceId** (String) - The device ID provided by the client. - **payload** (Payload) - Contains the request data, including `AuthenticationReq`. - **outPayload** (Payload) - Used to send response data back to the client. ### Request Body (within Payload) - **AuthenticationReq** (ProtoBuffer) - **accessToken** (String) - The access token for authentication. ### Response (within outPayload) - **AuthenticationRes** (ProtoBuffer) - **errorCode** (ErrorCode) - The result of the authentication attempt. ### Return Value - `true` if authentication is successful. - `false` if authentication fails. ### Example Usage ```java // Client sends: // accountId = "user123", password = "user123" // AuthenticationReq { accessToken: "TapTap_AccessToken_abc" } // Server responds: // AuthenticationRes { errorCode: NONE } → authentication granted ``` ``` -------------------------------- ### Database Access with UserDbHelperService (Java) Source: https://context7.com/nhn/gameanvil.sample-game-server/llms.txt Utilize UserDbHelperService for MyBatis integration, running JDBC calls off-fiber. Each method commits transactions internally on success. ```java UserDbHelperService db = UserDbHelperService.getInstance(); // All methods suspend the current fiber and execute JDBC on DB_THREAD_POOL GameUserInfo user = db.selectUserByUuid("user-uuid-1234"); int insertCount = db.insertUser(gameUserInfo); int updateNickname = db.updateUserNickname("user-uuid-1234", "NewNick"); int updateDeck = db.updateUserCurrentDeck("user-uuid-1234", "dragon"); int updateScore = db.updateUserHigScore("user-uuid-1234", 4200); // Each call commits the transaction internally on success (resultCount == 1) ``` -------------------------------- ### Server Configuration - GameAnvilConfig.json Source: https://context7.com/nhn/gameanvil.sample-game-server/llms.txt Declarative JSON configuration for the GameAnvil server, controlling node counts, ports, channel IDs, and cluster topology. This file is automatically loaded from the classpath at startup. ```json { "common": { "ip": "127.0.0.1", "meetEndPoints": ["127.0.0.1:18000"], "debugMode": false }, "location": { "clusterSize": 1, "replicaSize": 1, "shardFactor": 2 }, "match": { "nodeCnt": 1 }, "gateway": { "nodeCnt": 4, "ip": "127.0.0.1", "dns": "", "connectGroup": { "TCP_SOCKET": { "port": 18200, "idleClientTimeout": 240000 }, "WEB_SOCKET": { "port": 18300, "idleClientTimeout": 240000 } } }, "game": [{ "nodeCnt": 4, "serviceId": 1, "serviceName": "TapTap", "channelIDs": ["", "", "", ""], "userTimeout": 5000 }], "support": [{ "nodeCnt": 2, "serviceId": 2, "serviceName": "Launching", "restIp": "127.0.0.1", "restPort": 18600 }] } ``` -------------------------------- ### Snake Room Matchmaker Implementation Source: https://context7.com/nhn/gameanvil.sample-game-server/llms.txt Extends BaseUserMatchMaker for 2-player Snake. Groups players by rating and matches them. Adjusts match window if minimum players aren't met. ```java @ServiceName(GameConstants.GAME_NAME) @RoomType(GameConstants.GAME_ROOM_TYPE_MULTI_USER_MATCH) // "SnakeRoom" public class SnakeRoomMatchMaker extends BaseUserMatchMaker { public SnakeRoomMatchMaker() { super(2, 10000); // matchSize=2 players, timeout=10000ms } @Override public void onMatch() { int leastAmount = matchSize * currentMatchPoolFactor; // starts at 2 List requests = getMatchRequests(leastAmount); if (requests == null) { if (System.currentTimeMillis() - lastMatchTime >= 1000) { currentMatchPoolFactor = Math.max(currentMatchPoolFactor / 2, 1); } return; } // Group by rating bracket, then pair within each bracket Map> groups = new TreeMap<>(); for (SnakeUserMatchInfo info : requests) { groups.computeIfAbsent(info.getRating() / 100, k -> new ArrayList<>()).add(info); } groups.forEach((bracket, members) -> { int matched = matchSingles(members); if (matched > 0) { lastMatchTime = System.currentTimeMillis(); currentMatchPoolFactor = matchPoolFactorMax; } }); } } ``` -------------------------------- ### Unlimited Room Matchmaker Logic Source: https://context7.com/nhn/gameanvil.sample-game-server/llms.txt Implements matchmaking logic for unlimited rooms. It always allows matching and prioritizes older rooms. ```java @ServiceName(GameConstants.GAME_NAME) @RoomType(GameConstants.GAME_ROOM_TYPE_MULTI_ROOM_MATCH) public class UnlimitedTapRoomMatchMaker extends BaseRoomMatchMaker { @Override public boolean canMatch(UnlimitedTapRoomMatchForm form, UnlimitedTapRoomMatchInfo info, Object... args) { return true; // accept any room } @Override public int compare(UnlimitedTapRoomMatchInfo o1, UnlimitedTapRoomMatchInfo o2) { // Sort descending by age — oldest room first return Long.compare(o2.getCreateTime(), o1.getCreateTime()); } } ``` -------------------------------- ### Session Lifecycle - GameSession Source: https://context7.com/nhn/gameanvil.sample-game-server/llms.txt Manages session-specific lifecycle hooks including pre-login, post-login, and post-logout. ```APIDOC ## Session Lifecycle — `GameSession` ### Description The `@Session`-annotated class tracks login/logout hooks per-session. `onPreLogin` fires before the user object is created; `onPostLogin` fires after successful login. `onPostLogout` fires after the user has logged out. ### Methods - **`onPreLogin(Payload outPayload)`** - Fires before the user object is created. - Can be used to attach extra data to the payload. - **`onPostLogin()`** - Fires after the user has successfully logged in. - **`onPostLogout()`** - Fires after the user has logged out. - Used for cleanup operations. ### Example Usage ```java @Session public class GameSession extends BaseSession { @Override public void onPreLogin(Payload outPayload) throws SuspendExecution { // Attach extra data to the payload before the user's onLogin is called logger.info("Session preLogin for account: {}", getAccountId()); } @Override public void onPostLogin() throws SuspendExecution { // User is now fully logged in logger.info("Session postLogin for account: {}", getAccountId()); } @Override public void onPostLogout() throws SuspendExecution { // Cleanup after logout logger.info("Session postLogout for account: {}", getAccountId()); } } ``` ``` -------------------------------- ### Manage Session Lifecycle with GameSession Hooks Source: https://context7.com/nhn/gameanvil.sample-game-server/llms.txt Utilize `GameSession` to manage the lifecycle of a user's session. Implement `onPreLogin` for actions before user object creation, `onPostLogin` for actions after successful login, and `onPostLogout` for cleanup. ```java @Session public class GameSession extends BaseSession { @Override public void onPreLogin(Payload outPayload) throws SuspendExecution { // Attach extra data to the payload before the user's onLogin is called logger.info("Session preLogin for account: {}", getAccountId()); } @Override public void onPostLogin() throws SuspendExecution { // User is now fully logged in logger.info("Session postLogin for account: {}", getAccountId()); } @Override public void onPostLogout() throws SuspendExecution { // Cleanup after logout logger.info("Session postLogout for account: {}", getAccountId()); } } ``` -------------------------------- ### Create Unlimited Tap Room Source: https://context7.com/nhn/gameanvil.sample-game-server/llms.txt Initializes a new multiplayer room, adds players, and registers it with the matchmaker. This is called when a new room is created. ```java @ServiceName(GameConstants.GAME_NAME) @RoomType(GameConstants.GAME_ROOM_TYPE_MULTI_ROOM_MATCH) // "UnlimitedTapRoom" public class UnlimitedTapRoom extends BaseRoom { private Map gameUserMap; // userId → GameUser private Map gameUserScoreMap; // uuid → score @Override public boolean onCreateRoom(GameUser gameUser, Payload inPayload, Payload outPayload) throws SuspendExecution { gameUserMap.put(gameUser.getUserId(), gameUser); gameUserScoreMap.put(gameUser.getGameUserInfo().getUuid(), 0); unlimitedTapRoomMatchInfo.setCreateTime(System.currentTimeMillis()); registerRoomMatch(unlimitedTapRoomMatchInfo, "UNLIMITED_TAP", gameUser.getUserId()); // Registers this room with the MatchNode so other players can join outPayload.add(gameUser.getRoomInfoMsgByProto(RoomGameType.ROOM_TAP)); return true; } // Score broadcast (called from _ScoreUpMsg handler): public GameMulti.BroadcastTapBirdMsg getBroadcastMsgByProto() { List list = new ArrayList<>(); for (GameUser user : gameUserMap.values()) { TapBirdUserData.Builder d = TapBirdUserData.newBuilder(); d.setUserData(user.getRoomUserDataByProto( gameUserScoreMap.get(user.getGameUserInfo().getUuid()))); list.add(d.build()); } return GameMulti.BroadcastTapBirdMsg.newBuilder().addAllTapBirdData(list).build(); } } ``` -------------------------------- ### Redis Helper for Game Data Source: https://context7.com/nhn/gameanvil.sample-game-server/llms.txt Provides a wrapper for Lettuce cluster commands, integrating with GameAnvil's fiber-aware awaitFuture. Manages user data storage and leaderboard operations. ```java public class RedisHelper { // Connect once per GameNode in onInit() public void connect(String url, int port) throws SuspendExecution { RedisURI uri = RedisURI.Builder.redis(url, port).build(); // For password-protected Redis: // RedisURI uri = RedisURI.Builder.redis(url, port).withPassword("secret").build(); clusterClient = RedisClusterClient.create(Collections.singletonList(uri)); clusterConnection = Lettuce.connect(GameConstants.REDIS_THREAD_POOL, clusterClient); clusterAsyncCommands = clusterConnection.async(); } // Store user JSON under hash key "taptap_user_data", field = uuid public boolean setUserData(GameUserInfo info) throws SuspendExecution { String json = GameAnvilUtil.Gson().toJson(info); try { Lettuce.awaitFuture(clusterAsyncCommands.hset( REDIS_USER_DATA_KEY, info.getUuid(), json)); return true; } catch (TimeoutException e) { return false; } } // Store score in sorted set "taptap_single_score" (higher is better) public boolean setSingleScore(String uuid, double score) throws SuspendExecution { try { Lettuce.awaitFuture(clusterAsyncCommands.zadd( REDIS_SINGLE_SCORE_KEY, score, uuid)); return true; } catch (TimeoutException e) { return false; } } // Fetch top-N from sorted set, descending public Map getSingleRanking(int start, int end) throws SuspendExecution { RedisFuture>> future = clusterAsyncCommands.zrevrangeWithScores(REDIS_SINGLE_SCORE_KEY, start, end); // Returns: {"uuid1": {uuid, score:4200}, "uuid2": {uuid, score:3100}, ...} try { return Lettuce.awaitFuture(future.thenApplyAsync(rows -> { Map map = new LinkedHashMap<>(); for (ScoredValue sv : rows) { SingleRankingInfo r = new SingleRankingInfo(); r.setUuid(sv.getValue()); r.setScore(sv.getScore()); map.put(r.getUuid(), r); } return map; })); } catch (TimeoutException e) { return null; } } } ``` -------------------------------- ### Gateway Node - GameGatewayNode Class Source: https://context7.com/nhn/gameanvil.sample-game-server/llms.txt Handles node lifecycle events and optional timer callbacks for the gateway. Lifecycle methods run on Quasar fibers and throw SuspendExecution. Custom TCP messages are not handled at the gateway level in this sample. ```java @GatewayNode() public class GameGatewayNode extends BaseGatewayNode implements TimerHandler { @Override public void onInit() throws SuspendExecution { // Called once when the node initializes logger.info("GatewayNode onInit"); } @Override public void onReady() throws SuspendExecution { // Node is accepting connections logger.info("GatewayNode onReady"); } @Override public void onPause(Payload payload) throws SuspendExecution { // Non-stop patch: node is paused but not shut down } @Override public void onTimer(Timer timerObject, Object arg) throws SuspendExecution { // Custom periodic logic can be placed here logger.info("GatewayNode timer fired: {}", arg); } @Override public MessageDispatcher getMessageDispatcher() { return null; // No custom TCP messages at gateway level in this sample } } ``` -------------------------------- ### Implement Connection Authentication in GameConnection Source: https://context7.com/nhn/gameanvil.sample-game-server/llms.txt Implement the `onAuthenticate` method to validate client credentials and access tokens. Ensure the `AuthenticationReq` payload is correctly parsed and the access token has the expected prefix. ```java // Protocol definition (Authentication.proto) // message AuthenticationReq { string accessToken = 1; } // message AuthenticationRes { ErrorCode errorCode = 1; } @Connection() public class GameConnection extends BaseConnection { @Override public boolean onAuthenticate(String accountId, String password, String deviceId, Payload payload, Payload outPayload) throws SuspendExecution { Result.ErrorCode resultCode = ErrorCode.UNKNOWN; if (accountId.equals(password)) { Authentication.AuthenticationReq req = payload.getProtoBuffer(Authentication.AuthenticationReq.getDescriptor()); if (req == null) { resultCode = ErrorCode.PARAMETER_IS_EMPTY; } else if (req.getAccessToken() == null) { resultCode = ErrorCode.TOKEN_IS_EMPTY; } else if (req.getAccessToken().startsWith("TapTap_AccessToken")) { resultCode = ErrorCode.NONE; // success } } else { resultCode = ErrorCode.PASSWORD_NOT_MATCHED; } Authentication.AuthenticationRes.Builder res = Authentication.AuthenticationRes.newBuilder(); res.setErrorCode(resultCode); outPayload.add(res); return resultCode == ErrorCode.NONE; } } // Client sends: // accountId = "user123", password = "user123" // AuthenticationReq { accessToken: "TapTap_AccessToken_abc" } // Server responds: // AuthenticationRes { errorCode: NONE } → authentication granted ``` -------------------------------- ### Deserialize User State after Transfer - GameUser.onTransferIn() Source: https://context7.com/nhn/gameanvil.sample-game-server/llms.txt Restores the user's game state from a TransferPack after a node migration. ```java @Override public void onTransferIn(TransferPack transferPack) throws SuspendExecution { GameUserTransferInfo transferInfo = (GameUserTransferInfo) transferPack.get("GameUserInfo"); gameUserInfo = transferInfo.getGameUserInfo(); snakePositionInfoList = transferInfo.getGetSnakePositionInfoList(); } ``` -------------------------------- ### UserDbHelperService Database Operations Source: https://context7.com/nhn/gameanvil.sample-game-server/llms.txt An alternative fiber-safe database layer using MyBatis, executing JDBC calls off-fiber. ```APIDOC ## Database Access — `UserDbHelperService` (MyBatis, Fiber-safe) Alternative DB layer using MyBatis with `Async.callBlocking()` to run JDBC calls off-fiber in the `DB_THREAD_POOL`. Toggle via `GameConstants.USE_DB_JASYNC_SQL = false`. ```java // Singleton enum — safe to use from any fiber UserDbHelperService db = UserDbHelperService.getInstance(); // All methods suspend the current fiber and execute JDBC on DB_THREAD_POOL GameUserInfo user = db.selectUserByUuid("user-uuid-1234"); int insertCount = db.insertUser(gameUserInfo); int updateNickname = db.updateUserNickname("user-uuid-1234", "NewNick"); int updateDeck = db.updateUserCurrentDeck("user-uuid-1234", "dragon"); int updateScore = db.updateUserHigScore("user-uuid-1234", 4200); // Each call commits the transaction internally on success (resultCount == 1) ``` ``` -------------------------------- ### User Re-Login - GameUser.onReLogin() Source: https://context7.com/nhn/gameanvil.sample-game-server/llms.txt Called when a previously logged-in user reconnects without going through the full login flow. Returns the current in-memory user state immediately. ```APIDOC ## GameUser.onReLogin() ### Description Called when a previously logged-in user reconnects without going through the full login flow. Returns the current in-memory user state immediately. ### Method ```java @Override public boolean onReLogin(Payload payload, Payload accountPayload, Payload outPayload) throws SuspendExecution ``` ### Request Body - **payload** (Payload) - Not explicitly used for re-login logic but part of the method signature. - **accountPayload** (Payload) - Account-related payload. - **outPayload** (Payload) - Payload to be populated with the response. ### Response #### Success Response - **outPayload** will contain `LoginRes` with the current in-memory `UserData`. ### Request Example ```java Authentication.LoginRes.Builder loginRes = Authentication.LoginRes.newBuilder(); loginRes.setUserdata(getUserDataByProto()); // returns current in-memory state outPayload.add(loginRes); ``` ```