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/
/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:
# 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:
refid = RefID.from_userid(user.userid)
bound = Profile.from_userid(user.userid) is not None
@ -76,12 +84,13 @@ class CardMng(object):
{
"refid": refid.refid,
"dataid": refid.refid,
"newflag": "1",
"newflag": "0",
"binded": "1" if bound else "0",
"expired": "0",
"exflag": "0",
"useridflag": "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.builder import E
from v8_server import db
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.model.song import HitChart
from v8_server.model.user import User, UserAccount
logger = logging.getLogger(__name__)
class Local(object):
@ -21,10 +31,30 @@ class Local(object):
CARDUTIL = "cardutil"
CARDUTIL_CHECK = "check"
CARDUTIL_REGIST = "regist"
GAMEINFO = "gameinfo"
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
def shopinfo(cls, req: ServiceRequest) -> etree:
"""
@ -110,6 +140,9 @@ class Local(object):
"""
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:
<call model="K32:J:B:A:2011033000" srcid="00010203040506070809">
<demodata method="get">
@ -125,27 +158,40 @@ class Local(object):
<demodata expire="600"/>
</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:
# 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(
E.demodata(
E.hitchart({"nr": "3"}),
E.data(
E.musicid("133", e_type(T.s32)),
E.last1("1", e_type(T.s32)),
E.mode("0", e_type(T.u8)), # Unknown what mode we need
E.hitchart(
E.start(datetime.now().strftime(dtfmt)),
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.musicid("208", e_type(T.s32)),
E.last1("2", e_type(T.s32)),
),
E.data(
E.musicid("209", e_type(T.s32)),
E.last1("3", e_type(T.s32)),
E.bossdata( # No idea what this stuff means
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.info(E.message("SenPi's Kickass Machine")),
E.assert_report_state("0", e_type(T.u8)),
)
)
else:
@ -186,6 +232,30 @@ class Local(object):
response = E.response(
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:
raise Exception(
"Not sure how to handle this cardutil request. "
@ -217,7 +287,48 @@ class Local(object):
</response>
"""
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:
raise Exception(
"Not sure how to handle this gameinfo request. "
@ -225,3 +336,46 @@ class Local(object):
)
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
# BINARY = 8
# DLSTATUS = 9
# EACOIN = 10
DLSTATUS = 9
EACOIN = 10
# EEMALL = 11
# INFO = 12
# LOBBY = 13
# NETLOG = 14
LOBBY = 13
NETLOG = 14
# NUMBERING = 15
# PKGLIST = 16
# POSEVENT = 17
# REFERENCE = 18
# SHOPINF = 19
# SIDMGR = 20
# USERDATA = 21
# USERID = 22
SIDMGR = 20
USERDATA = 21
USERID = 22
TRACEROUTE = 23
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
# 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
# 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 sqlalchemy import JSON, Column, ForeignKey, UniqueConstraint
from sqlalchemy.orm import relationship
from sqlalchemy.types import Integer, String
from sqlalchemy.types import Boolean, Integer, String
from v8_server import db
@ -27,9 +27,10 @@ class User(BaseModel):
userid = Column(Integer, nullable=False, primary_key=True)
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")
refids = relationship("RefID", back_populates="user")
user_account = relationship("UserAccount", uselist=False, back_populates="user")
def __repr__(self) -> str:
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()
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):
"""
@ -51,7 +71,7 @@ class Card(BaseModel):
cardid = Column(String(16), nullable=False, primary_key=True)
userid = Column(Integer, ForeignKey("users.userid"), nullable=False)
user = relationship("User", back_populates="cards")
user = relationship("User", back_populates="card")
def __repr__(self) -> str:
return f'Card<cardid: "{self.cardid}", userid: {self.userid}>'
@ -64,7 +84,7 @@ class ExtID(BaseModel):
"""
__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)
game = Column(String(32), nullable=False)
userid = Column(Integer, ForeignKey("users.userid"), nullable=False)

View File

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