feat: implement game logic
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
from enum import Enum
|
||||
|
||||
import polars as pl
|
||||
from google.protobuf.message import Message
|
||||
from star_resonance_tracer.proto.enum_place_holder_type_pb2 import PlaceHolderType
|
||||
from star_resonance_tracer.proto.stru_place_holder_buff_pb2 import PlaceHolderBuff
|
||||
from star_resonance_tracer.proto.stru_place_holder_fish_item_pb2 import PlaceHolderFishItem
|
||||
from star_resonance_tracer.proto.stru_place_holder_fish_personal_total_pb2 import PlaceHolderFishPersonalTotal
|
||||
from star_resonance_tracer.proto.stru_place_holder_fish_rank_pb2 import PlaceHolderFishRank
|
||||
from star_resonance_tracer.proto.stru_place_holder_item_pb2 import PlaceHolderItem
|
||||
from star_resonance_tracer.proto.stru_place_holder_master_mode_pb2 import PlaceHolderMasterMode
|
||||
from star_resonance_tracer.proto.stru_place_holder_pb2 import PlaceHolder
|
||||
from star_resonance_tracer.proto.stru_place_holder_player_pb2 import PlaceHolderPlayer
|
||||
from star_resonance_tracer.proto.stru_place_holder_scene_position_pb2 import PlaceHolderScenePosition
|
||||
from star_resonance_tracer.proto.stru_place_holder_str_pb2 import PlaceHolderStr
|
||||
from star_resonance_tracer.proto.stru_place_holder_timestamp_pb2 import PlaceHolderTimestamp
|
||||
from star_resonance_tracer.proto.stru_place_holder_union_pb2 import PlaceHolderUnion
|
||||
from star_resonance_tracer.proto.stru_place_holder_val_pb2 import PlaceHolderVal
|
||||
|
||||
__all__ = (
|
||||
"ChitChatNtf",
|
||||
"HypertextVariant",
|
||||
"decode_placeholder",
|
||||
"get_item_name"
|
||||
)
|
||||
|
||||
PLACEHOLDER_MAPPING: dict[PlaceHolderType, type[Message]] = {
|
||||
PlaceHolderType.PlaceHolderTypeVal: PlaceHolderVal,
|
||||
PlaceHolderType.PlaceHolderTypePlayer: PlaceHolderPlayer,
|
||||
PlaceHolderType.PlaceHolderTypeItem: PlaceHolderItem,
|
||||
PlaceHolderType.PlaceHolderTypeUnion: PlaceHolderUnion,
|
||||
PlaceHolderType.PlaceHolderTypeBuff: PlaceHolderBuff,
|
||||
PlaceHolderType.PlaceHolderTypeTimestamp: PlaceHolderTimestamp,
|
||||
PlaceHolderType.PlaceHolderTypeString: PlaceHolderStr,
|
||||
PlaceHolderType.PlaceHolderTypeFishPersonalTotal: PlaceHolderFishPersonalTotal,
|
||||
PlaceHolderType.PlaceHolderTypeFishItem: PlaceHolderFishItem,
|
||||
PlaceHolderType.PlaceHolderTypeFishRank: PlaceHolderFishRank,
|
||||
PlaceHolderType.PlaceHolderTypeMasterMode: PlaceHolderMasterMode,
|
||||
PlaceHolderType.PlaceHolderTypeScenePosition: PlaceHolderScenePosition,
|
||||
}
|
||||
|
||||
type SupportedPlaceholders = (
|
||||
PlaceHolderVal
|
||||
| PlaceHolderPlayer
|
||||
| PlaceHolderItem
|
||||
| PlaceHolderUnion
|
||||
| PlaceHolderBuff
|
||||
| PlaceHolderTimestamp
|
||||
| PlaceHolderStr
|
||||
| PlaceHolderFishPersonalTotal
|
||||
| PlaceHolderFishItem
|
||||
| PlaceHolderFishRank
|
||||
| PlaceHolderMasterMode
|
||||
| PlaceHolderScenePosition
|
||||
)
|
||||
|
||||
|
||||
class ChitChatNtf(Enum):
|
||||
ServiceId = 0x9d4a768
|
||||
|
||||
class Method(Enum):
|
||||
NotifyNewestChitChatMsgs = 0x1
|
||||
|
||||
|
||||
class HypertextVariant(Enum):
|
||||
ITEM_SHARING = 3000001
|
||||
|
||||
|
||||
def decode_placeholder(placeholder: PlaceHolder) -> SupportedPlaceholders:
|
||||
decoder = PLACEHOLDER_MAPPING.get(placeholder.type)
|
||||
if decoder is None:
|
||||
raise NotImplementedError
|
||||
|
||||
# All compiled protobuf messages support ``FromString``
|
||||
return decoder.FromString(placeholder.bytesContent) # type: ignore[attr-defined]
|
||||
|
||||
|
||||
ITEM_NAME = pl.read_json("./ref/StarResonanceData/ztable/ItemTable.json").transpose().unnest()
|
||||
|
||||
|
||||
def get_item_name(item_config_id: int) -> str | None:
|
||||
try:
|
||||
return ITEM_NAME.filter(pl.col.Id == item_config_id).select("Name").item()
|
||||
except ValueError:
|
||||
return None
|
||||
@@ -0,0 +1,77 @@
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
import logging
|
||||
from star_resonance_tracer.proto.stru_place_holder_item_pb2 import PlaceHolderItem
|
||||
from star_resonance_tracer.sniffer import Sniffer
|
||||
|
||||
from inventory_wars.models import User, ItemShare, Event
|
||||
from inventory_wars.const import ChitChatNtf, HypertextVariant, decode_placeholder
|
||||
from star_resonance_tracer.proto.serv_chit_chat_ntf_pb2 import ChitChatNtf as ChitChatNtfPb
|
||||
from star_resonance_tracer.proto.enum_chit_chat_channel_type_pb2 import ChitChatChannelType
|
||||
from star_resonance_tracer.proto.enum_chit_chat_msg_type_pb2 import ChitChatMsgType
|
||||
|
||||
from inventory_wars.sniffer import start_sniffing
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Game:
|
||||
def __init__(self, session: Session, *, item_id: int, listening_channels: list[ChitChatChannelType] | None = None):
|
||||
self.session = session
|
||||
self.listening_channels = listening_channels or []
|
||||
self.event = Event(item_id=item_id)
|
||||
|
||||
def start(self) -> None:
|
||||
self.event.timestamp = datetime.now()
|
||||
|
||||
self.session.add(self.event)
|
||||
self.session.commit()
|
||||
|
||||
logger.info(f"Started {self.event.id} at {self.event.timestamp} for item {self.event.item_id}")
|
||||
|
||||
sniffer = Sniffer()
|
||||
sniffer.set_service_type(
|
||||
ChitChatNtf.ServiceId.value,
|
||||
ChitChatNtf.Method.NotifyNewestChitChatMsgs.value,
|
||||
ChitChatNtfPb.NotifyNewestChitChatMsgs
|
||||
)
|
||||
sniffer.on_service(ChitChatNtfPb.NotifyNewestChitChatMsgs, self.on_chit_chat_msg)
|
||||
|
||||
start_sniffing(sniffer)
|
||||
|
||||
def on_chit_chat_msg(self, event: ChitChatNtfPb.NotifyNewestChitChatMsgs) -> None:
|
||||
req = event.vRequest
|
||||
if req.channelType not in self.listening_channels:
|
||||
return
|
||||
|
||||
if req.chatMsg.msgInfo.msgType is not ChitChatMsgType.ChatMsgHypertext:
|
||||
return
|
||||
|
||||
hypertext = req.chatMsg.msgInfo.chatHypertext
|
||||
if hypertext.configId != HypertextVariant.ITEM_SHARING.value:
|
||||
return
|
||||
|
||||
for placeholder in hypertext.hypertextContents:
|
||||
placeholder_content = decode_placeholder(placeholder)
|
||||
match placeholder_content:
|
||||
case PlaceHolderItem() as item:
|
||||
if item.configId != self.event.item_id:
|
||||
continue
|
||||
|
||||
user = User(id=req.chatMsg.sendCharInfo.charID, username=req.chatMsg.sendCharInfo.name)
|
||||
item_share = ItemShare(
|
||||
timestamp=datetime.fromtimestamp(req.chatMsg.timestamp),
|
||||
user_id=user.id,
|
||||
event_id=self.event.id,
|
||||
count=item.ItemDetail.count,
|
||||
raw=item.SerializeToString()
|
||||
)
|
||||
|
||||
self.session.merge(user)
|
||||
self.session.add(item_share)
|
||||
self.session.commit()
|
||||
|
||||
logger.info(f"{user.username} guessed {self.event.item_id} "
|
||||
f"with {item_share.count} at {item_share.timestamp}")
|
||||
@@ -0,0 +1,50 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, UniqueConstraint, BLOB
|
||||
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
||||
|
||||
__all__ = (
|
||||
"Base",
|
||||
"User",
|
||||
"Event",
|
||||
"ItemShare"
|
||||
)
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "user"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
username: Mapped[str]
|
||||
|
||||
shares: Mapped[set[ItemShare]] = relationship(back_populates="user")
|
||||
|
||||
|
||||
class Event(Base):
|
||||
__tablename__ = "event"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||
item_id: Mapped[int]
|
||||
timestamp: Mapped[datetime] = mapped_column(DateTime(timezone=False))
|
||||
|
||||
shares: Mapped[set[ItemShare]] = relationship(back_populates="event")
|
||||
|
||||
|
||||
class ItemShare(Base):
|
||||
__tablename__ = "item_share"
|
||||
|
||||
timestamp: Mapped[datetime] = mapped_column(DateTime(timezone=False), primary_key=True)
|
||||
user_id: Mapped[int] = mapped_column(ForeignKey("user.id"))
|
||||
event_id: Mapped[int] = mapped_column(ForeignKey("event.id"))
|
||||
count: Mapped[int]
|
||||
raw: Mapped[bytes] = mapped_column(BLOB())
|
||||
|
||||
user: Mapped[User] = relationship(back_populates="shares")
|
||||
event: Mapped[Event] = relationship(back_populates="shares")
|
||||
@@ -0,0 +1,51 @@
|
||||
import logging
|
||||
import os
|
||||
|
||||
from star_resonance_tracer.sniffer import Sniffer
|
||||
from star_resonance_tracer.connection import Connection, PidBasedConnectionDetector
|
||||
from star_resonance_tracer.utils import TCPReassembler
|
||||
from collections import defaultdict
|
||||
from scapy.config import conf
|
||||
from scapy.layers.inet import TCP, IP
|
||||
from scapy.packet import Packet, Raw
|
||||
from scapy.sendrecv import sniff
|
||||
|
||||
__all__ = (
|
||||
"start_sniffing",
|
||||
)
|
||||
|
||||
conf.layers.filter([TCP, IP])
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def start_sniffing(sniffer: Sniffer) -> None:
|
||||
executable_name = "StarSEA"
|
||||
|
||||
detector = PidBasedConnectionDetector()
|
||||
assert detector.add_from_executable_name(executable_name)
|
||||
reassemblers: dict[Connection, TCPReassembler] = defaultdict(TCPReassembler)
|
||||
|
||||
def on_packet(packet: Packet):
|
||||
if TCP not in packet or Raw not in packet:
|
||||
return
|
||||
|
||||
tcp = packet[TCP]
|
||||
ip = packet[IP]
|
||||
|
||||
connection = Connection.from_tuple(ip.src, tcp.sport, ip.dst, tcp.dport)
|
||||
payload = bytes(packet[Raw])
|
||||
|
||||
try:
|
||||
payload = reassemblers[connection].push(tcp.seq, payload)
|
||||
except KeyError:
|
||||
return
|
||||
|
||||
if not payload:
|
||||
return
|
||||
|
||||
try:
|
||||
sniffer.process_packet(payload)
|
||||
except Exception:
|
||||
logger.exception("Silently caught exception")
|
||||
|
||||
sniff(filter=detector.as_bpf_filter(), prn=on_packet, store=False)
|
||||
Reference in New Issue
Block a user