Skip to content

Thiết kế: Game & Room Management Feature

1. Tổng quan kiến trúc

flowchart TB
    subgraph Client["Mobile Client"]
        WS_CMD["WebSocket Command\n(CREATE/JOIN/READY/QUIT/...)"]
        REST["REST API\n/api/game/* /api/room/*"]
    end

    subgraph WebSocket["websocket module (port 8081)"]
        EP["/websocket/{token}\n@ServerEndpoint"]
        CC["ConsumerContainer\n@OnCmd dispatcher"]
        RCC["RoomCmdConsumer\n@CmdConsumer"]
        CRS["CreateRoomService"]
    end

    subgraph MQ["ActiveMQ Artemis"]
        PAQ_P["queue-prize-allocation-PRIZE"]
        PAQ_PS["queue-prize-allocation-PRIZE_FOR_STREAMER"]
        PAQ_UN["queue-prize-allocation-UNLIMITED"]
        MQ_MM["queue-game-mode-type-pool-{machineId}"]
    end

    subgraph Batch["batch module"]
        PAL["JMS*AllocationListeners"]
        CRJ["CreateRoomJMSService"]
        DYN["ProgrammaticEndpointRegistration\n(startup: per machine queues)"]
    end

    subgraph Core["application-core"]
        RRMS["RRoomService\n(Redis Room State)"]
        VALD["CreateRoomValidationService"]
        BCAST["BroadcastPubSubService"]
    end

    WS_CMD --> EP --> CC --> RCC
    REST --> GameController
    RCC --> CRS
    CRS --> VALD
    CRS -->|"UNLIMITED"| PAQ_UN
    CRS -->|"PRIZE"| PAQ_P
    CRS -->|"PRIZE_FOR_STREAMER"| PAQ_PS
    CRS -->|"machine-specific"| MQ_MM

    PAQ_P --> PAL
    PAQ_PS --> PAL
    PAQ_UN --> PAL
    MQ_MM --> CRJ
    PAL --> CRJ
    CRJ --> RRMS
    CRJ --> BCAST
    BCAST -->|"broadcast event"| Client

    DYN -->|"register at startup"| MQ_MM

2. Domain model

RRoomModel (Redis room state)

String roomId;
String gameMode;         // PVE_FREE_PLAY | PVE_SINGLE_PLAY | PVP | ...
RoomStatus status;       // WAITING | READY | PLAYING | FINISHED | CANCELLED
Map<String, Player> players;  // userId → Player
String machineId;
String prizeId;
String playableGameBoothSettingId;
Instant createTime;
Instant startTime;
// ...

CreateRoomData

String userId;
String machineId;
String prizeId;
List<String> prizeIds;   // multi-match
String playableGameBoothSettingId;
CreateRoomActionType actionType;  // CREATE_PVE_FREE_PLAY | CREATE_PVE_SINGLE_PLAY | ...
PrizeEnum.AllocationType allocationType;
boolean skipLifeGauge;
boolean fakeMachine;
RoomSyncRegister roomSyncRegister;
// ...

3. Sequence diagrams

3.1 CREATE_PVE_SINGLE_PLAY_ROOM

sequenceDiagram
    participant C as Client
    participant CC as ConsumerContainer
    participant RC as RoomCmdConsumer
    participant CRS as CreateRoomService
    participant VAL as CreateRoomValidationService
    participant SEND as SendPrizeAllocationMessageService
    participant Q as queue-prize-allocation-PRIZE
    participant L as JMSPrizeAllocationListener
    participant CRJ as CreateRoomJMSService
    participant BCAST as BroadcastPubSubService

    C->>CC: CREATE_PVE_SINGLE_PLAY_ROOM\n{playableGameBoothSettingId, prizeId, machineId}
    CC->>RC: onCreatePveSinglePlayRoom(objectMessage, userId)
    RC->>CRS: createPveSinglePlayRoom(req, userId)
    CRS->>VAL: validate(userId, prizeId, machineId, ...)
    VAL-->>CRS: ValidationResult (pass/fail + reason)

    alt Validation failed
        CRS->>BCAST: broadcast VALIDATE_GAME + reason
        BCAST-->>C: {event_type: VALIDATE_GAME, reason: "REASON_CODE"}
    else Validation passed
        CRS->>CRS: determineAllocationType(prizeId)
        Note over CRS: reservedType=STREAMING → PRIZE_FOR_STREAMER\nDefault → PRIZE
        CRS->>SEND: sendMessageWithActionType(CREATE_PVE_SINGLE_PLAY, PRIZE)
        SEND->>Q: enqueue PrizeAllocationData
        Q->>L: onMessage(messageJson)
        L->>L: validate allocationType == PRIZE
        L->>CRJ: createPveSinglePlayRoom(createRoomData)
        CRJ->>CRJ: create/update RRoomModel in Redis
        CRJ->>BCAST: broadcast ROOM_CREATED + room info
        BCAST-->>C: {event_type: ROOM_CREATED, room: {...}}
    end

3.2 QUIT_GAME

sequenceDiagram
    participant C as Client
    participant RC as RoomCmdConsumer
    participant CRS as CreateRoomService
    participant Q as queue-prize-allocation-*
    participant L as JMS Listener

    C->>RC: QUIT_GAME {roomId}
    RC->>RC: validate room exists + user in room
    RC->>CRS: quitGame(userId, roomId)
    CRS->>CRS: buildRollbackData(userId, room)
    CRS->>Q: sendDecreaseMessage(prizeRollbackData)
    Q->>L: process DECREASE action
    L->>L: rollback inventory/allocation
    RC->>RC: cleanup room state (Redis)

3.3 Dynamic queue registration (startup)

sequenceDiagram
    participant App as Batch App Startup
    participant REG as ProgrammaticEndpointRegistration
    participant DB as MySQL (machine/game_mode records)
    participant MQ as ActiveMQ Artemis
    participant L as GameModeCreateRoomJMSListener

    App->>REG: @PostConstruct init()
    REG->>DB: load all machine records
    REG->>DB: load all playableGameBoothSetting records
    loop per machineId/settingId
        REG->>MQ: registerListener(queue-game-mode-type-pool-{id})
        MQ-->>L: new listener registered
    end
    Note over REG: Dynamic queues allow per-machine\nsequential processing

4. Room state machine

stateDiagram-v2
    [*] --> WAITING: CREATE command

    WAITING --> WAITING: JOIN (room not full)
    WAITING --> READY: All players READY
    READY --> PLAYING: GAME_START signal
    PLAYING --> FINISHED: Game ends normally
    PLAYING --> CANCELLED: QUIT_GAME / timeout
    WAITING --> CANCELLED: CANCEL_GAME

    FINISHED --> [*]
    CANCELLED --> [*]

    note right of WAITING: Broadcast: ROOM_CREATED\nBroadcast: PLAYER_JOINED
    note right of READY: Broadcast: ROOM_READY
    note right of PLAYING: Broadcast: GAME_START
    note right of FINISHED: Broadcast: GAME_END\nPrize distribution triggered
    note right of CANCELLED: Resource rollback triggered

5. Queue allocation routing logic

// Trong CreateRoomService.determineAllocationType(prizeId)
PrizeEnum.AllocationType determineType(String prizeId) {
    PrizeModel prize = prizeService.findById(prizeId);

    if (prize.getReservedType() == STREAMING) {
        return PRIZE_FOR_STREAMER;
    }
    if (prize.getPrizeType() == TREASURE_BOX || prize.getPrizeType() == INVITATION_CARD) {
        return UNLIMITED;  // non-streaming treasure/invitation
    }
    // PVE_FREE_PLAY always UNLIMITED (set in RoomCmdConsumer before calling)
    return PRIZE;  // default
}

6. Error handling

Situation Event type Reason code
User đang ở room khác VALIDATE_GAME ROOM_ALREADY_EXIST
Prize không tồn tại VALIDATE_GAME INVALID_PRIZE_ID
Insufficient balance VALIDATE_GAME INSUFFICIENT_BALANCE
Machine không available VALIDATE_GAME MACHINE_NOT_AVAILABLE
Room đầy VALIDATE_GAME ROOM_IS_FULL
WsCmdFailException VALIDATE_GAME exception message

7. WebSocket command dispatch

// ConsumerContainer dispatch mechanism
// Annotation-based routing:

@CmdConsumer  // marks class as consumer
public class RoomCmdConsumer {

    @OnCmd("CREATE_PVE_SINGLE_PLAY_ROOM")  // maps command string to method
    public void onCreatePveSinglePlayRoom(JSONObject objectMessage, String userId) {
        // ...
    }

    @OnCmd("JOIN_PVE_FREE_PLAY_ROOM")
    public void onJoinFreePlayRoom(JSONObject objectMessage, String userId) {
        // ...
    }
}

8. Điểm cần chú ý

# Vấn đề Chi tiết
1 Room state trong Redis Không persist qua restart batch service
2 Dynamic queue registration Cần chạy lại khi thêm machine/game mode mới
3 skipLifeGauge / fakeMachine Cần guard không cho dùng trên production
4 Queue lag Nếu PRIZE queue đầy → tạo room bị delay