1
0

feat: implement game logic

This commit is contained in:
2026-06-01 22:33:12 +08:00
parent 0619f8c429
commit 48b69b7775
11 changed files with 406 additions and 3 deletions
View File
+85
View File
@@ -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
+77
View File
@@ -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}")
+50
View File
@@ -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")
+51
View File
@@ -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)