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 |