Skip to content

Commit

Permalink
[#37] feature: apply binary payload format to socket.io server
Browse files Browse the repository at this point in the history
- integrate with FlatBuffers
- binary payload generaated on switch-character event

Signed-off-by: T.H. <[email protected]>
  • Loading branch information
metalalive committed Feb 6, 2024
1 parent 2994db9 commit 7a07047
Show file tree
Hide file tree
Showing 11 changed files with 615 additions and 416 deletions.
12 changes: 4 additions & 8 deletions backend/app/adapter/event_emitter.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from typing import Dict
from typing import Dict, Union
import logging
import socketio

from app.dto import Investigator, Difficulty
from app.dto import Investigator, Difficulty, RtcCharacterMsgData
from app.config import LOG_FILE_PATH, RTC_HOST, RTC_PORT
from app.constant import RealTimeCommConst as RtcConst, GameRtcEvent
from app.domain import Game
Expand Down Expand Up @@ -38,18 +38,14 @@ async def create_game(self, game: Game):
async def switch_character(
self, game_id: str, player_id: str, new_invstg: Investigator
):
data = {
"gameID": game_id,
"player_id": player_id,
"investigator": new_invstg.value,
}
data = RtcCharacterMsgData.serialize(game_id, player_id, new_invstg)
await self.do_emit(data, evt=RtcConst.EVENTS.CHARACTER)

async def update_difficulty(self, game_id: str, level: Difficulty):
data = {"gameID": game_id, "level": level.value}
await self.do_emit(data, evt=RtcConst.EVENTS.DIFFICULTY)

async def do_emit(self, data: Dict, evt: GameRtcEvent):
async def do_emit(self, data: Union[Dict, bytes], evt: GameRtcEvent):
try:
if not self._client.connected:
await self._client.connect(self._url, namespace=self._namespace)
Expand Down
44 changes: 29 additions & 15 deletions backend/app/adapter/sio_srv.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import logging
import asyncio
import os
from typing import Dict
from typing import Dict, Callable, Union

import socketio
from hypercorn.config import Config
from hypercorn.asyncio import serve
from pydantic import BaseModel, ValidationError
from pydantic import ValidationError

from app.constant import RealTimeCommConst as RtcConst, GameRtcEvent
from app.config import RTC_HOST, RTC_PORT, LOG_FILE_PATH
Expand All @@ -33,24 +33,35 @@
_repo = get_rtc_room_repository()


async def _generic_forward_msg(sid, data: Dict, evt: GameRtcEvent, msgtype: BaseModel):
async def _generic_forward_msg(
sid, data: Union[Dict, bytes], evt: GameRtcEvent, validator: Callable
):
try:
validated = msgtype(**data)
await srv.emit(
evt.value,
data,
namespace=RtcConst.NAMESPACE,
room=validated.gameID,
skip_sid=sid,
)
if isinstance(data, bytes): # FlatBuffers encoded
validated = validator(data)
elif isinstance(data, Dict): # JSON encoded
validated = validator(**data)
else:
_logger.error("incorrect-data-type:%s", type(data))
validated = None
if validated:
await srv.emit(
evt.value,
data,
namespace=RtcConst.NAMESPACE,
room=validated.gameID,
skip_sid=sid,
)
except ValidationError as e:
error = e.errors(include_url=False, include_input=False)
await srv.emit(evt.value, data=error, namespace=RtcConst.NAMESPACE, to=sid)


@srv.on(RtcConst.EVENTS.CHAT.value, namespace=RtcConst.NAMESPACE)
async def _forward_chat_msg(sid, data: Dict):
await _generic_forward_msg(sid, data, evt=RtcConst.EVENTS.CHAT, msgtype=ChatMsgData)
await _generic_forward_msg(
sid, data, evt=RtcConst.EVENTS.CHAT, validator=ChatMsgData
)


async def check_room_exist(repo, data: RtcInitMsgData):
Expand Down Expand Up @@ -145,18 +156,21 @@ async def _new_game_room(sid, data: Dict):


@srv.on(RtcConst.EVENTS.CHARACTER.value, namespace=RtcConst.NAMESPACE)
async def _player_switch_character(sid, data: Dict):
async def _player_switch_character(sid, data: bytes):
# TODO, ensure this event is sent by authorized http server
await _generic_forward_msg(
sid, data, evt=RtcConst.EVENTS.CHARACTER, msgtype=RtcCharacterMsgData
sid,
data,
evt=RtcConst.EVENTS.CHARACTER,
validator=RtcCharacterMsgData.deserialize,
)


@srv.on(RtcConst.EVENTS.DIFFICULTY.value, namespace=RtcConst.NAMESPACE)
async def _update_game_difficulty(sid, data: Dict):
# TODO, ensure this event is sent by authorized http server
await _generic_forward_msg(
sid, data, evt=RtcConst.EVENTS.DIFFICULTY, msgtype=RtcDifficultyMsgData
sid, data, evt=RtcConst.EVENTS.DIFFICULTY, validator=RtcDifficultyMsgData
)


Expand Down
104 changes: 0 additions & 104 deletions backend/app/dto.py

This file was deleted.

168 changes: 168 additions & 0 deletions backend/app/dto/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
from enum import Enum
from typing import List
from pydantic import BaseModel, RootModel, ConfigDict
import flatbuffers

from .rtc import CharacterSelection, Investigator as InvestigatorFbs

# data transfer objects (DTO) in the application
# TODO, determine module path


class PlayerDto(BaseModel):
model_config = ConfigDict(extra="forbid")
id: str
nickname: str


class CreateGameReqDto(BaseModel):
model_config = ConfigDict(extra="forbid")
players: List[PlayerDto]


class CreateGameRespDto(BaseModel):
url: str


class Investigator(Enum):
# 偵探
DETECTIVE = "detective"
# 博士
DOCTOR = "doctor"
# 司機
DRIVER = "driver"
# 獵人
HUNTER = "hunter"
# 魔術師
MAGICIAN = "magician"
# 神祕學家
OCCULTIST = "occultist"
# 記者
REPORTER = "reporter"

@classmethod
def from_fbs(cls, value):
fbs = InvestigatorFbs.Investigator()
match value:
case fbs.DETECTIVE:
return cls.DETECTIVE
case fbs.DOCTOR:
return cls.DOCTOR
case fbs.DRIVER:
return cls.DRIVER
case fbs.HUNTER:
return cls.HUNTER
case fbs.MAGICIAN:
return cls.MAGICIAN
case fbs.OCCULTIST:
return cls.OCCULTIST
case fbs.REPORTER:
return cls.REPORTER

def to_fbs(self):
fbs = InvestigatorFbs.Investigator()
match self:
case self.DETECTIVE:
return fbs.DETECTIVE
case self.DOCTOR:
return fbs.DOCTOR
case self.DRIVER:
return fbs.DRIVER
case self.HUNTER:
return fbs.HUNTER
case self.MAGICIAN:
return fbs.MAGICIAN
case self.OCCULTIST:
return fbs.OCCULTIST
case self.REPORTER:
return fbs.REPORTER


class SingleInvestigatorDto(BaseModel):
model_config = ConfigDict(extra="forbid")
investigator: Investigator


ListInvestigatorsDto = RootModel[List[SingleInvestigatorDto]]


class UpdateInvestigatorDto(BaseModel):
model_config = ConfigDict(extra="forbid")
investigator: Investigator
player_id: str


class Difficulty(Enum):
# 教學難度
INTRODUCTORY = "introductory"
# 標準難度
STANDARD = "standard"
# 專家難度
EXPERT = "expert"


class UpdateDifficultyDto(BaseModel):
model_config = ConfigDict(extra="forbid")
level: Difficulty


class UpdateCommonRespDto(BaseModel):
message: str


class RtcRoomMsgData(BaseModel):
model_config = ConfigDict(extra="forbid")
gameID: str
players: List[str]


class ChatMsgData(BaseModel):
model_config = ConfigDict(extra="forbid")
msg: str
nickname: str # TODO, replace with PlayerDto
gameID: str
client: str ## client session ID


class RtcInitMsgData(BaseModel):
model_config = ConfigDict(extra="forbid")
player: PlayerDto
gameID: str
client: str # client session ID


class RtcCharacterMsgData(BaseModel):
model_config = ConfigDict(extra="forbid")
gameID: str
investigator: Investigator
player_id: str

def serialize(game_id: str, player_id: str, new_invstg: Investigator) -> bytes:
builder = flatbuffers.Builder(256)
game_id = builder.CreateString(game_id)
player_id = builder.CreateString(player_id)
investigator = new_invstg.to_fbs()
CharacterSelection.Start(builder)
CharacterSelection.AddGameId(builder, game_id)
CharacterSelection.AddPlayerId(builder, player_id)
CharacterSelection.AddInvestigator(builder, investigator)
selection = CharacterSelection.End(builder)
builder.Finish(selection)
serial = builder.Output() # byte-array
return bytes(serial)

def deserialize(data: bytes):
buf = bytearray(data)
obj = CharacterSelection.CharacterSelection.GetRootAs(buf, offset=0)
game_id = obj.GameId().decode("utf-8")
player = obj.PlayerId().decode("utf-8")
investigator = Investigator.from_fbs(obj.Investigator())
return RtcCharacterMsgData(
gameID=game_id, investigator=investigator, player_id=player
)


class RtcDifficultyMsgData(BaseModel):
model_config = ConfigDict(extra="forbid")
gameID: str
level: Difficulty
Loading

0 comments on commit 7a07047

Please sign in to comment.