Have a bunch of stuff working. Next is gametop

This commit is contained in:
573dev 2020-11-02 08:10:29 -06:00
parent 27117aa5c4
commit ca2f46dce6
9 changed files with 337 additions and 29 deletions

3
.gitignore vendored
View File

@ -107,3 +107,6 @@ ENV/
/decompiled/ /decompiled/
/database/ /database/
# Ignore this for now, not sure I want to just throw it up on github yet
/v8_server/model/data/mdb.json

View File

@ -63,7 +63,15 @@ class CardMng(object):
if user is None: if user is None:
# The user doesn't exist, force system to create a new account # The user doesn't exist, force system to create a new account
response = E.response(E.cardmng({"status": str(CardMng.NOT_REGISTERED)})) response = E.response(
E.cardmng(
{
"newflag": "1",
"binded": "0",
"status": str(CardMng.NOT_REGISTERED),
}
)
)
else: else:
refid = RefID.from_userid(user.userid) refid = RefID.from_userid(user.userid)
bound = Profile.from_userid(user.userid) is not None bound = Profile.from_userid(user.userid) is not None
@ -76,12 +84,13 @@ class CardMng(object):
{ {
"refid": refid.refid, "refid": refid.refid,
"dataid": refid.refid, "dataid": refid.refid,
"newflag": "1", "newflag": "0",
"binded": "1" if bound else "0", "binded": "1" if bound else "0",
"expired": "0", "expired": "0",
"exflag": "0", "exflag": "0",
"useridflag": "1", "useridflag": "1",
"extidflag": "1", "extidflag": "1",
"status": str(CardMng.SUCCESS),
} }
) )
) )

View File

@ -0,0 +1 @@
# >>libshare-pj.c: xrpc_module_add(aLobby, "lobby", aLobby, &off_100404C4);

View File

@ -1,8 +1,18 @@
import logging
from datetime import datetime
from lxml import etree from lxml import etree
from lxml.builder import E from lxml.builder import E
from v8_server import db
from v8_server.eamuse.services.services import ServiceRequest from v8_server.eamuse.services.services import ServiceRequest
from v8_server.eamuse.utils.crc import calculate_crc8
from v8_server.eamuse.utils.xml import XMLBinTypes as T, e_type from v8_server.eamuse.utils.xml import XMLBinTypes as T, e_type
from v8_server.model.song import HitChart
from v8_server.model.user import User, UserAccount
logger = logging.getLogger(__name__)
class Local(object): class Local(object):
@ -21,10 +31,30 @@ class Local(object):
CARDUTIL = "cardutil" CARDUTIL = "cardutil"
CARDUTIL_CHECK = "check" CARDUTIL_CHECK = "check"
CARDUTIL_REGIST = "regist"
GAMEINFO = "gameinfo" GAMEINFO = "gameinfo"
GAMEINFO_GET = "get" GAMEINFO_GET = "get"
GAMEEND = "gameend"
GAMEEND_REGIST = "regist"
GAMEINFO = "gameinfo"
GAMEINFO_GET = "get"
GAMETOP = "gametop"
GAMETOP_GET = "get"
GAMETOP_GET_RIVAL = "get_rival"
CUSTOMIZE = "customize"
CUSTOMIZE_REGIST = "regist"
ASSERT_REPORT = "assert_report"
ASSERT_REPORT_REGIST = "regist"
# Not sure about this one yet
INCREMENT = "increment"
@classmethod @classmethod
def shopinfo(cls, req: ServiceRequest) -> etree: def shopinfo(cls, req: ServiceRequest) -> etree:
""" """
@ -110,6 +140,9 @@ class Local(object):
""" """
Handle the demodata request. Handle the demodata request.
Potentially this is just some initial demo data for initial boot/factory reset
After this data, the game might keep track of all this stuff itself.
# Example Request: # Example Request:
<call model="K32:J:B:A:2011033000" srcid="00010203040506070809"> <call model="K32:J:B:A:2011033000" srcid="00010203040506070809">
<demodata method="get"> <demodata method="get">
@ -125,27 +158,40 @@ class Local(object):
<demodata expire="600"/> <demodata expire="600"/>
</response> </response>
""" """
# TODO: Figure out what this thing actually needs to send back dtfmt = "%Y-%m-%d %H:%M:%S%z"
if req.method == cls.DEMODATA_GET: if req.method == cls.DEMODATA_GET:
# response = E.response(E.demodata({"expire": "600"})) hitchart_number = int(req.xml[0].find("hitchart_nr").text)
rank_data = HitChart.get_ranking(hitchart_number)
# try some dummy response that might have some info in it
response = E.response( response = E.response(
E.demodata( E.demodata(
E.hitchart({"nr": "3"}), E.mode("0", e_type(T.u8)), # Unknown what mode we need
E.data( E.hitchart(
E.musicid("133", e_type(T.s32)), E.start(datetime.now().strftime(dtfmt)),
E.last1("1", e_type(T.s32)), E.end(datetime.now().strftime(dtfmt)),
*[
E.data(
E.musicid(str(x), e_type(T.s32)),
E.last1("0", e_type(T.s32)),
)
for x in rank_data
],
{"nr": str(hitchart_number)},
), ),
E.data( E.bossdata( # No idea what this stuff means
E.musicid("208", e_type(T.s32)), E.division("14", e_type(T.u8)), # Shows up as "Extra Lv X"
E.last1("2", e_type(T.s32)), E.border("0 0 0 0 0 0 0 0 0", e_type(T.u8, count=9)),
), E.extra_border("90", e_type(T.u8)),
E.data( E.bsc_encore_border("92", e_type(T.u8)),
E.musicid("209", e_type(T.s32)), E.adv_encore_border("93", e_type(T.u8)),
E.last1("3", e_type(T.s32)), E.ext_encore_border("94", e_type(T.u8)),
E.bsc_premium_border("95", e_type(T.u8)),
E.adv_premium_border("95", e_type(T.u8)),
E.ext_premium_border("95", e_type(T.u8)),
), ),
E.info(E.message("SenPi's Kickass Machine")),
E.assert_report_state("0", e_type(T.u8)),
) )
) )
else: else:
@ -186,6 +232,30 @@ class Local(object):
response = E.response( response = E.response(
E.cardutil(E.card(E.kind("0", e_type(T.s8)), {"no": "1", "state": "0"})) E.cardutil(E.card(E.kind("0", e_type(T.s8)), {"no": "1", "state": "0"}))
) )
elif req.method == cls.CARDUTIL_REGIST:
root = req.xml[0].find("data")
refid = root.find("refid").text
name = root.find("name").text
chara = root.find("chara").text
cardid = root.find("uid").text
is_succession = root.find("is_succession").text
user = User.from_refid(refid)
if user is None:
raise Exception("This user should theoretically exist here")
if user.card.cardid != cardid:
raise Exception(f"Card ID is incorrect: {user.card.cardid} != {cardid}")
user_account = UserAccount(
userid=user.userid,
name=name,
chara=int(chara),
is_succession=True if is_succession == "1" else False,
)
db.session.add(user_account)
db.session.commit()
response = E.response(E.cardutil())
else: else:
raise Exception( raise Exception(
"Not sure how to handle this cardutil request. " "Not sure how to handle this cardutil request. "
@ -217,7 +287,48 @@ class Local(object):
</response> </response>
""" """
if req.method == cls.GAMEINFO_GET: if req.method == cls.GAMEINFO_GET:
response = E.response(E.gameinfo()) response = E.response(
E.gameinfo(
E.mode("0", e_type(T.u8)),
E.free_music("262143", e_type(T.u32)),
E.key(E.musicid("-1", e_type(T.s32))),
E.limit_gdp("40000", e_type(T.u32)),
E.free_chara("1824", e_type(T.u32)),
E.tag(str(calculate_crc8(str(262143 + 1824))), e_type(T.u8)),
E.bossdata(
E.division("14", e_type(T.u8)), # Shows up as "Extra Lv X"
E.border("0 0 0 0 0 0 0 0 0", e_type(T.u8, count=9)),
E.extra_border("90", e_type(T.u8)),
E.bsc_encore_border("92", e_type(T.u8)),
E.adv_encore_border("93", e_type(T.u8)),
E.ext_encore_border("94", e_type(T.u8)),
E.bsc_premium_border("95", e_type(T.u8)),
E.adv_premium_border("95", e_type(T.u8)),
E.ext_premium_border("95", e_type(T.u8)),
),
E.battledata(
E.battle_music_level(
" ".join("0" * 13), e_type(T.u8, count=13)
),
E.standard_skill(" ".join("0" * 13), e_type(T.s32, count=13)),
E.border_skill(" ".join("0" * 13), e_type(T.s32, count=13)),
),
E.quest(
E.division("0", e_type(T.u8)),
E.border("0", e_type(T.u8)),
E.qdata(" ".join("0" * 26), e_type(T.u32, count=26)),
*[
E(f"play_{x}", " ".join("0" * 32), e_type(T.u32, count=32))
for x in range(0, 13)
],
*[
E(f"clear_{x}", " ".join("0" * 32), e_type(T.u32, count=32))
for x in range(0, 13)
],
),
E.campaign(E.campaign("0", e_type(T.u8))),
)
)
else: else:
raise Exception( raise Exception(
"Not sure how to handle this gameinfo request. " "Not sure how to handle this gameinfo request. "
@ -225,3 +336,46 @@ class Local(object):
) )
return response return response
@classmethod
def gametop(cls, req: ServiceRequest) -> etree:
if req.method == cls.GAMETOP_GET:
pass
elif req.method == cls.GAMETOP_GET_RIVAL:
pass
else:
raise Exception(
"Not sure how to handle this gametop request. "
f'method "{req.method}" is unknown for request: {req}'
)
return request
@classmethod
def gameend(cls, req: ServiceRequest) -> etree:
"""
Handle a GameEnd request.
For now just save the hitchart data
"""
if req.method == cls.GAMEEND_REGIST:
# Grab the hitchart data
hc_root = req.xml[0].find("hitchart")
for mid in hc_root.findall("musicid"):
musicid = int(mid.text)
hc = HitChart(musicid=musicid, playdate=datetime.now())
logger.debug(f"Saving HitChart: {hc}")
db.session.add(hc)
db.session.commit()
# Just send back a dummy object for now
response = E.response(E.gameend())
else:
raise Exception(
"Not sure how to handle this gameend request. "
f'method "{req.method}" is unknown for request: {req}'
)
return response

View File

@ -38,20 +38,21 @@ class ServiceType(IntEnum):
# Extra for testing # Extra for testing
# BINARY = 8 # BINARY = 8
# DLSTATUS = 9 DLSTATUS = 9
# EACOIN = 10 EACOIN = 10
# EEMALL = 11 # EEMALL = 11
# INFO = 12 # INFO = 12
# LOBBY = 13 LOBBY = 13
# NETLOG = 14 NETLOG = 14
# NUMBERING = 15 # NUMBERING = 15
# PKGLIST = 16 # PKGLIST = 16
# POSEVENT = 17 # POSEVENT = 17
# REFERENCE = 18 # REFERENCE = 18
# SHOPINF = 19 # SHOPINF = 19
# SIDMGR = 20 SIDMGR = 20
# USERDATA = 21 USERDATA = 21
# USERID = 22 USERID = 22
TRACEROUTE = 23
class Services(object): class Services(object):
@ -66,7 +67,8 @@ class Services(object):
# Default service url that GFDM uses. You will need to set up your network so that # Default service url that GFDM uses. You will need to set up your network so that
# this URL points to this server. # this URL points to this server.
SERVICE_URL = "https://eamuse.konami.fun" # SERVICE_URL = "https://eamuse.konami.fun"
SERVICE_URL = "https://e.k.f"
# The base route that GFDM uses to query the eAmuse server to get the list of # The base route that GFDM uses to query the eAmuse server to get the list of
# offered services # offered services

View File

@ -0,0 +1,14 @@
def calculate_crc8(value: str) -> int:
"""
Calculate the CRC8 of a string representation of an int
"""
crc = 0
for c in bytearray(value.encode("ASCII")):
for i in range(8, 0, -1):
t = c ^ crc
crc >>= 1
if (t & 0x01) != 0:
crc ^= 0x8C
c >>= 1
return crc

103
v8_server/model/song.py Normal file
View File

@ -0,0 +1,103 @@
from __future__ import annotations
import json
import logging
from datetime import datetime
from pathlib import Path
from typing import List, Tuple
from flask_sqlalchemy.model import DefaultMeta
from sqlalchemy import Column, ForeignKey, PrimaryKeyConstraint, event, func, text
from sqlalchemy.orm import relationship
from sqlalchemy.types import DateTime, Integer, String
from v8_server import db
BaseModel: DefaultMeta = db.Model
logger = logging.getLogger(__name__)
class Song(BaseModel):
"""
Table representing the song data
"""
__tablename__ = "songs"
musicid = Column(Integer, nullable=False, primary_key=True)
bpm = Column(Integer, nullable=False)
title_ascii = Column(String(16), nullable=False)
hitcharts = relationship("HitChart", back_populates="song")
def __repr__(self) -> str:
return (
f"Song<musicid: {self.musicid}, bpm: {self.bpm}, "
f'title_ascii: "{self.title_ascii}">'
)
class HitChart(BaseModel):
"""
Table representing the song hit chart
"""
__tablename__ = "hitchart"
__table_args__ = (PrimaryKeyConstraint("musicid", "playdate"),)
musicid = Column(Integer, ForeignKey("songs.musicid"), nullable=False)
playdate = Column(DateTime, nullable=False)
song = relationship("Song", back_populates="hitcharts")
def __repr__(self) -> str:
return f"HitChart<musicid: {self.musicid}, playdate: {self.playdate}>"
@classmethod
def get_ranking(cls, count) -> List[int]:
items = (
db.session.query(
HitChart.musicid, func.count(HitChart.musicid).label("count")
)
.group_by(HitChart.musicid)
.order_by(text("count DESC"))
.order_by(HitChart.musicid.desc())
.limit(count)
.all()
)
results = []
for item in items:
results.append(item[0])
return results
def insert_initial_song_data(target, connection, **kwargs) -> None:
# Make sure both Song and HitChart have been created
try:
db.session.execute("SELECT * FROM songs")
db.session.execute("SELECT * FROM hitchart")
except Exception:
return
# Load up the mdb.json file
data_path = Path(__file__).parent / "data" / "mdb.json"
with data_path.open() as f:
json_data = json.loads(f.read())
for key, song in json_data["musicdb"]["songs"].items():
song_obj = Song(musicid=key, bpm=song["bpm"], title_ascii=song["title_ascii"])
db.session.add(song_obj)
db.session.commit()
# Insert initial hitchart data, just add one entry for every song with the current
# timestamp
now = datetime.now()
for key, song in json_data["musicdb"]["songs"].items():
hc = HitChart(musicid=key, playdate=now)
db.session.add(hc)
db.session.commit()
event.listen(Song.__table__, "after_create", insert_initial_song_data)
event.listen(HitChart.__table__, "after_create", insert_initial_song_data)

View File

@ -6,7 +6,7 @@ from typing import Optional
from flask_sqlalchemy.model import DefaultMeta from flask_sqlalchemy.model import DefaultMeta
from sqlalchemy import JSON, Column, ForeignKey, UniqueConstraint from sqlalchemy import JSON, Column, ForeignKey, UniqueConstraint
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from sqlalchemy.types import Integer, String from sqlalchemy.types import Boolean, Integer, String
from v8_server import db from v8_server import db
@ -27,9 +27,10 @@ class User(BaseModel):
userid = Column(Integer, nullable=False, primary_key=True) userid = Column(Integer, nullable=False, primary_key=True)
pin = Column(String(4), nullable=False) pin = Column(String(4), nullable=False)
cards = relationship("Card", back_populates="user") card = relationship("Card", uselist=False, back_populates="user")
extids = relationship("ExtID", back_populates="user") extids = relationship("ExtID", back_populates="user")
refids = relationship("RefID", back_populates="user") refids = relationship("RefID", back_populates="user")
user_account = relationship("UserAccount", uselist=False, back_populates="user")
def __repr__(self) -> str: def __repr__(self) -> str:
return f'User<userid: {self.userid}, pin: "{self.pin}">' return f'User<userid: {self.userid}, pin: "{self.pin}">'
@ -39,6 +40,25 @@ class User(BaseModel):
card = db.session.query(Card).filter(Card.cardid == cardid).one_or_none() card = db.session.query(Card).filter(Card.cardid == cardid).one_or_none()
return card.user if card is not None else None return card.user if card is not None else None
@classmethod
def from_refid(cls, refid: str) -> Optional[User]:
ref = db.session.query(RefID).filter(RefID.refid == refid).one_or_none()
return ref.user if ref is not None else None
class UserAccount(BaseModel):
"""
Table representing a user account.
"""
__tablename__ = "user_accounts"
userid = Column(Integer, ForeignKey("users.userid"), primary_key=True)
name = Column(String(8), nullable=False)
chara = Column(Integer, nullable=False)
is_succession = Column(Boolean, nullable=False)
user = relationship("User", back_populates="user_account")
class Card(BaseModel): class Card(BaseModel):
""" """
@ -51,7 +71,7 @@ class Card(BaseModel):
cardid = Column(String(16), nullable=False, primary_key=True) cardid = Column(String(16), nullable=False, primary_key=True)
userid = Column(Integer, ForeignKey("users.userid"), nullable=False) userid = Column(Integer, ForeignKey("users.userid"), nullable=False)
user = relationship("User", back_populates="cards") user = relationship("User", back_populates="card")
def __repr__(self) -> str: def __repr__(self) -> str:
return f'Card<cardid: "{self.cardid}", userid: {self.userid}>' return f'Card<cardid: "{self.cardid}", userid: {self.userid}>'
@ -64,7 +84,7 @@ class ExtID(BaseModel):
""" """
__tablename__ = "extids" __tablename__ = "extids"
__table_args_ = (UniqueConstraint("game", "userid", name="game_userid"),) __table_args__ = (UniqueConstraint("game", "userid", name="game_userid"),)
extid = Column(Integer, nullable=False, primary_key=True) extid = Column(Integer, nullable=False, primary_key=True)
game = Column(String(32), nullable=False) game = Column(String(32), nullable=False)
userid = Column(Integer, ForeignKey("users.userid"), nullable=False) userid = Column(Integer, ForeignKey("users.userid"), nullable=False)

View File

@ -145,6 +145,8 @@ def local_service() -> FlaskResponse:
response = Local.cardutil(req) response = Local.cardutil(req)
elif req.module == Local.GAMEINFO: elif req.module == Local.GAMEINFO:
response = Local.gameinfo(req) response = Local.gameinfo(req)
elif req.module == Local.GAMEEND:
response = Local.gameend(req)
else: else:
raise Exception(f"Not sure how to handle this Local Request: {req}") raise Exception(f"Not sure how to handle this Local Request: {req}")
return req.response(response) return req.response(response)