Back to parity with old code

This commit is contained in:
573dev 2020-10-30 22:33:58 -05:00
parent f34716a1c3
commit 27117aa5c4
13 changed files with 454 additions and 246 deletions

8
.gitignore vendored
View File

@ -102,8 +102,8 @@ ENV/
# mypy
.mypy_cache/
# logs
logs/
# project related things
/logs/
/decompiled/
/database/
# Decompiled v8 code
decompiled/

View File

@ -9,6 +9,7 @@ DOCS_DEPS = ["sphinx", "sphinx-rtd-theme", "sphinx-autoapi", "recommonmark"]
CHECK_DEPS = ["isort", "flake8", "flake8-quotes", "pep8-naming", "mypy", "black"]
REQUIREMENTS = [
"flask",
"flask_sqlalchemy",
"watchdog",
"pyopenssl",
"lxml",

View File

@ -5,20 +5,14 @@ from pathlib import Path
from typing import Optional, Union
from flask import Flask, has_request_context, request
from flask_sqlalchemy import SQLAlchemy
from v8_server.config import Development, Production
from v8_server.model.connection import Base, Database
from v8_server.model.user import User
from v8_server.utils.flask import generate_secret_key
from .version import __version__
# Make sure the database has been created
with Database() as db:
Base.metadata.create_all(db.engine)
class RequestFormatter(logging.Formatter):
def format(self, record):
encrypted = "None"
@ -117,9 +111,13 @@ static_dir = str(package_dir / "static")
app = Flask(__name__, template_folder=template_dir, static_folder=static_dir)
app.secret_key = generate_secret_key(config.SECRET_KEY_FILENAME)
app.config.from_object(config)
db = SQLAlchemy(app)
# Make sure the database has been created
db.create_all()
# We need to import the views here specifically once the flask app has been initialized
import v8_server.view # noqa: F401, E402
__all__ = ["__version__", "app", "LOG_PATH"]
__all__ = ["__version__", "app", "db", "LOG_PATH"]

View File

@ -1,13 +1,23 @@
from pathlib import Path
DEV_DB_PATH = Path(__file__).parent.parent / "database"
PROD_DB_PATH = Path("/var/db")
class Config(object):
DEBUG: bool = False
TESTING: bool = False
DB_SERVER: str = "localhost"
SECRET_KEY_FILENAME: str = "v8_server.key"
SQLALCHEMY_DATABASE_URI: str = f"sqlite+pysqlite:///{ PROD_DB_PATH / 'v8.db'}"
SQLALCHEMY_TRACK_MODIFICATIONS = False
class Development(Config):
DEBUG: bool = True
SECRET_KEY_FILENAME: str = "dev_v8_server.key"
SQLALCHEMY_DATABASE_URI: str = f"sqlite+pysqlite:///{ DEV_DB_PATH / 'v8_dev.db'}"
class Production(Config):

View File

@ -1,3 +1,4 @@
from v8_server.eamuse.services.cardmng import CardMng
from v8_server.eamuse.services.facility import Facility
from v8_server.eamuse.services.local import Local
from v8_server.eamuse.services.message import Message
@ -8,6 +9,7 @@ from v8_server.eamuse.services.services import ServiceRequest, Services, Service
__all__ = [
"CardMng",
"Facility",
"Local",
"Message",

View File

@ -0,0 +1,165 @@
from lxml.builder import E
from v8_server import db
from v8_server.eamuse.services.services import ServiceRequest
from v8_server.eamuse.utils.xml import get_xml_attrib
from v8_server.model.user import Card, Profile, RefID, User
class CardMng(object):
"""
Handle the CardMng (Card Manage) request.
This is for supporting eAmuse card interaction.
"""
# List of statuses we return to the game for various card related reasons
SUCCESS = 0
NO_PROFILE = 109
NOT_ALLOWED = 110
NOT_REGISTERED = 112
INVALID_PIN = 116
# Methods
INQUIRE = "inquire"
GETREFID = "getrefid"
AUTHPASS = "authpass"
BINDMODEL = "bindmodel"
GETKEEPSPAN = "getkeepspan"
GETDATALIST = "getdatalist"
@classmethod
def inquire(cls, req: ServiceRequest):
"""
Example Request:
<call model="K32:J:B:A:2011033000" srcid="00010203040506070809">
<cardmng
cardid="E0040100DE52896C"
cardtype="1"
method="inquire"
update="1"
/>
</call>
Example Response:
<response>
<cardmng
refid="ADE0FE0B14AEAEFC"
dataid="ADE0FE0B14AEAEFC"
newflag="1"
binded="0"
expired="0"
ecflag="0"
useridflag="1"
extidflag="1"
/>
</response>
"""
# Grab the card id
cardid = get_xml_attrib(req.xml[0], "cardid")
# Check if a user with this card id already exists
user = User.from_cardid(cardid)
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)}))
else:
refid = RefID.from_userid(user.userid)
bound = Profile.from_userid(user.userid) is not None
if refid is None:
raise Exception("RefID Should not be None here!")
response = E.response(
E.cardmng(
{
"refid": refid.refid,
"dataid": refid.refid,
"newflag": "1",
"binded": "1" if bound else "0",
"expired": "0",
"exflag": "0",
"useridflag": "1",
"extidflag": "1",
}
)
)
return response
@classmethod
def getrefid(cls, req: ServiceRequest):
""""""
# Grab the card id and pin
cardid = get_xml_attrib(req.xml[0], "cardid")
pin = get_xml_attrib(req.xml[0], "passwd")
# Create a new user object with the given pin
user = User(pin=pin)
db.session.add(user)
db.session.commit()
# Create the card, tie it to the user account
card = Card(cardid=cardid, userid=user.userid)
db.session.add(card)
db.session.commit()
# Generate the refid and return it
refid = RefID.create_with_userid(user.userid)
return E.response(E.cardmng({"dataid": refid.refid, "refid": refid.refid}))
@classmethod
def authpass(cls, req: ServiceRequest):
""""""
# Grab the refid and pin
refid_str = get_xml_attrib(req.xml[0], "refid")
pin = get_xml_attrib(req.xml[0], "pass")
# Grab the refid
refid = db.session.query(RefID).filter(RefID.refid == refid_str).one_or_none()
if refid is None:
raise Exception("RefID Is None Here!")
# Check if the pin is valid for the user
user = refid.user
valid = user.pin == pin
return E.response(
E.cardmng(
{"status": str(CardMng.SUCCESS if valid else CardMng.INVALID_PIN)}
)
)
@classmethod
def bindmodel(cls, req: ServiceRequest):
""""""
# Grab the refid
refid_str = get_xml_attrib(req.xml[0], "refid")
refid = db.session.query(RefID).filter(RefID.refid == refid_str).one_or_none()
if refid is None:
raise Exception("RefID is None Here!")
# Just bind some garbage here for now
profile = Profile(refid=refid.refid, data={"data": "something"})
db.session.add(profile)
db.session.commit()
return E.response(E.cardmng({"dataid": refid.refid}))
@classmethod
def getkeepspan(cls):
"""
Unclear what this method does, return an arbitrary span
"""
return E.response(E.cardmng({"keepspan": "30"}))
@classmethod
def getdatalist(cls):
"""
Unclear what this method does, return a dummy response
"""
return E.response(E.cardmng())

View File

@ -22,6 +22,9 @@ class Local(object):
CARDUTIL = "cardutil"
CARDUTIL_CHECK = "check"
GAMEINFO = "gameinfo"
GAMEINFO_GET = "get"
@classmethod
def shopinfo(cls, req: ServiceRequest) -> etree:
"""
@ -125,7 +128,26 @@ class Local(object):
# TODO: Figure out what this thing actually needs to send back
if req.method == cls.DEMODATA_GET:
response = E.response(E.demodata({"expire": "600"}))
# response = E.response(E.demodata({"expire": "600"}))
# 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.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)),
),
)
)
else:
raise Exception(
"Not sure how to handle this demodata request. "
@ -153,7 +175,7 @@ class Local(object):
<response>
<cardutil>
<card no="1" state="0">
<kind __type="s8")0</kind>
<kind __type="s8">0</kind>
</card>
</cardutil>
</response>
@ -171,3 +193,35 @@ class Local(object):
)
return response
@classmethod
def gameinfo(cls, req: ServiceRequest) -> etree:
"""
Handle a Gameinfo request.
Currently unsure how to handle this, so we just return a dummy object.
# Example Request:
<call model="K32:J:B:A:2011033000" srcid="00010203040506070809">
<gameinfo method="get">
<shop>
<locationid __type="str">US-123</locationid>
<cabid __type="u32">1</cabid>
</shop>
</gameinfo>
</call>
Example Response:
<response>
<gameinfo expire="600"/>
</response>
"""
if req.method == cls.GAMEINFO_GET:
response = E.response(E.gameinfo())
else:
raise Exception(
"Not sure how to handle this gameinfo request. "
f'method "{req.method}" is unknown for request: {req}'
)
return response

View File

@ -36,6 +36,23 @@ class ServiceType(IntEnum):
CARDMNG = 6
LOCAL = 7
# Extra for testing
# BINARY = 8
# DLSTATUS = 9
# EACOIN = 10
# EEMALL = 11
# INFO = 12
# LOBBY = 13
# NETLOG = 14
# NUMBERING = 15
# PKGLIST = 16
# POSEVENT = 17
# REFERENCE = 18
# SHOPINF = 19
# SIDMGR = 20
# USERDATA = 21
# USERID = 22
class Services(object):
"""

View File

@ -0,0 +1,57 @@
import struct
from typing import Dict, List, Tuple
from v8_server.eamuse.utils.lz77 import Lz77
class ARC:
"""
Class representing an `.arc` file. These are found in DDR Ace, and possibly
other games that use ESS. Given a serires of bytes, this will allow you to
query included filenames as well as read the contents of any file inside the
archive.
"""
def __init__(self, data: bytes) -> None:
self.__files: Dict[str, Tuple[int, int, int]] = {}
self.__data = data
self.__parse_file(data)
def __parse_file(self, data: bytes) -> None:
# Check file header
if data[0:4] != bytes([0x20, 0x11, 0x75, 0x19]):
raise Exception("Unknown file format!")
# Grab header offsets
(_, numfiles, _) = struct.unpack("<III", data[4:16])
for fno in range(numfiles):
start = 16 + (16 * fno)
end = start + 16
(nameoffset, fileoffset, uncompressedsize, compressedsize) = struct.unpack(
"<IIII", data[start:end]
)
name = ""
while data[nameoffset] != 0:
name = name + data[nameoffset : (nameoffset + 1)].decode("ascii")
nameoffset = nameoffset + 1
self.__files[name] = (fileoffset, uncompressedsize, compressedsize)
@property
def filenames(self) -> List[str]:
return [f for f in self.__files]
def read_file(self, filename: str) -> bytes:
(fileoffset, uncompressedsize, compressedsize) = self.__files[filename]
if compressedsize == uncompressedsize:
# Just stored
return self.__data[fileoffset : (fileoffset + compressedsize)]
else:
# Compressed
lz77 = Lz77()
return lz77.decompress(
self.__data[fileoffset : (fileoffset + compressedsize)]
)

View File

@ -1,34 +0,0 @@
from __future__ import annotations
from pathlib import Path
from types import TracebackType
from typing import Optional, Type
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import DeclarativeMeta, declarative_base
from sqlalchemy.orm import Session as AlchemySession
Base: DeclarativeMeta = declarative_base()
class Database(object):
def __init__(self, echo: bool = False) -> None:
self.uri = f"sqlite+pysqlite:///{Path(__file__).parent / 'v8.db'}"
self.engine = create_engine(self.uri, echo=echo)
self.session = AlchemySession(bind=self.engine)
def __enter__(self) -> Database:
return self
def __exit__(
self,
exception_type: Optional[Type[BaseException]],
exception_value: Optional[BaseException],
traceback: Optional[TracebackType],
) -> bool:
self.session.close()
return exception_type is None
def __del__(self) -> None:
self.engine.dispose()

View File

@ -1,11 +1,23 @@
from __future__ import annotations
import random
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 v8_server.model.connection import Base
from v8_server import db
class User(Base):
BaseModel: DefaultMeta = db.Model
DEFAULT_GAME = "GFDM"
DEFAULT_VERSION = "v8"
class User(BaseModel):
"""
Table representing a user. Each user has a Unique ID and a pin which is used
with all cards associated with the user's account.
@ -13,17 +25,22 @@ class User(Base):
__tablename__ = "users"
uid = Column(Integer, nullable=False, primary_key=True)
userid = Column(Integer, nullable=False, primary_key=True)
pin = Column(String(4), nullable=False)
cards = relationship("Card", back_populates="user")
extids = relationship("ExtID", back_populates="user")
refids = relationship("RefID", back_populates="user")
def __repr__(self) -> str:
return f'User<uid: {self.uid}, pin: "{self.pin}">'
return f'User<userid: {self.userid}, pin: "{self.pin}">'
@classmethod
def from_cardid(cls, cardid: str) -> Optional[User]:
card = db.session.query(Card).filter(Card.cardid == cardid).one_or_none()
return card.user if card is not None else None
class Card(Base):
class Card(BaseModel):
"""
Table representing a card associated with a user. Users may have zero or more cards
associated with them. When a new card is used in a game, a new user will be created
@ -32,32 +49,56 @@ class Card(Base):
__tablename__ = "cards"
uid = Column(String(16), nullable=False, primary_key=True)
user_id = Column(Integer, ForeignKey("users.uid"), nullable=False)
cardid = Column(String(16), nullable=False, primary_key=True)
userid = Column(Integer, ForeignKey("users.userid"), nullable=False)
user = relationship("User", back_populates="cards")
def __repr__(self) -> str:
return f'Card<uid: "{self.uid}", user_id: {self.user_id}>'
return f'Card<cardid: "{self.cardid}", userid: {self.userid}>'
class ExtID(Base):
class ExtID(BaseModel):
"""
Table representing and extid for a user across a game series. Each game series on
the network gets its own extid (8 digit number) for each user.
"""
__tablename__ = "extids"
__table_args_ = (UniqueConstraint("game", "user_id", name="game_user_id"),)
uid = Column(Integer, nullable=False, primary_key=True)
__table_args_ = (UniqueConstraint("game", "userid", name="game_userid"),)
extid = Column(Integer, nullable=False, primary_key=True)
game = Column(String(32), nullable=False)
user_id = Column(Integer, ForeignKey("users.uid"), nullable=False)
userid = Column(Integer, ForeignKey("users.userid"), nullable=False)
user = relationship("User", back_populates="extids")
def __repr__(self) -> str:
return f'ExtID<uid: {self.uid}, game: "{self.game}", user_id: {self.user_id}>'
return f'ExtID<extid: {self.extid}, game: "{self.game}", userid: {self.userid}>'
@classmethod
def create_with_userid(cls, userid: int) -> Optional[ExtID]:
# First check if this user has an ExtID for GFDM
extid = (
db.session.query(ExtID)
.filter(ExtID.userid == userid, ExtID.game == DEFAULT_GAME)
.one_or_none()
)
if extid is None:
# Create a new ExtID that is unique
while True:
extid_val = random.randint(0, 89999999) + 10000000
count = db.session.query(ExtID).filter(ExtID.extid == extid_val).count()
if count == 0:
break
# Use this ExtID
extid = ExtID(extid=extid_val, game=DEFAULT_GAME, userid=userid)
db.session.add(extid)
db.session.commit()
return extid
class RefID(Base):
class RefID(BaseModel):
"""
Table representing a refid for a user. Each unique game on the network will need
a refid for each user/game/version they have a profile for. If a user does not have
@ -70,28 +111,81 @@ class RefID(Base):
__tablename__ = "refids"
__table_args__ = (
UniqueConstraint("game", "version", "user_id", name="game_version_user_id"),
UniqueConstraint("game", "version", "userid", name="game_version_userid"),
)
uid = Column(String(16), nullable=False, primary_key=True)
refid = Column(String(16), nullable=False, primary_key=True)
game = Column(String(32), nullable=False)
version = Column(Integer, nullable=False)
user_id = Column(Integer, ForeignKey("users.uid"), nullable=False)
userid = Column(Integer, ForeignKey("users.userid"), nullable=False)
user = relationship("User", back_populates="refids")
def __repr__(self) -> str:
return (
f'RefID<uid: {self.uid}, game: "{self.game}", version: {self.version} '
f"user_id: {self.user_id}>"
f'RefID<refid: {self.refid}, game: "{self.game}", version: {self.version} '
f"userid: {self.userid}>"
)
@classmethod
def from_userid(cls, userid: int) -> Optional[RefID]:
refid = (
db.session.query(RefID)
.filter(
RefID.userid == userid,
RefID.game == DEFAULT_GAME,
RefID.version == DEFAULT_VERSION,
)
.one_or_none()
)
class Profile(Base):
return refid
@classmethod
def create_with_userid(cls, userid: int) -> RefID:
# Create the ExtID
# This method will return an already existing ExtID or create a new one and
# return it. In this case we don't care what it returns
_ = ExtID.create_with_userid(userid)
# Create a new RefID that is unique
while True:
refid_val = "".join(random.choice("0123456789ABCDEF") for _ in range(16))
count = db.session.query(RefID).filter(RefID.refid == refid_val).count()
if count == 0:
break
# Use our newly created RefID
refid = RefID(
refid=refid_val, game=DEFAULT_GAME, version=DEFAULT_VERSION, userid=userid
)
db.session.add(refid)
db.session.commit()
return refid
class Profile(BaseModel):
"""
Table for storing JSON profile blobs, indexed by refid
"""
__tablename__ = "profiles"
ref_id = Column(
String(16), ForeignKey("refids.uid"), nullable=False, primary_key=True
refid = Column(
String(16), ForeignKey("refids.refid"), nullable=False, primary_key=True
)
data = Column(JSON, nullable=False)
@classmethod
def from_refid(cls, refid: str) -> Optional[Profile]:
return db.session.query(Profile).filter(Profile.refid == refid).one_or_none()
@classmethod
def from_userid(cls, userid: int) -> Optional[Profile]:
"""
Returns a user profile if it exists, or None if it doesn't
"""
refid = RefID.from_userid(userid)
if refid is None:
return None
return Profile.from_refid(refid.refid)

Binary file not shown.

View File

@ -6,6 +6,7 @@ from sqlalchemy.orm.exc import MultipleResultsFound
from v8_server import app
from v8_server.eamuse.services import (
CardMng,
Facility,
Local,
Message,
@ -17,7 +18,6 @@ from v8_server.eamuse.services import (
ServiceType,
)
from v8_server.eamuse.utils.xml import get_xml_attrib
from v8_server.model.connection import Database
from v8_server.model.user import Card, ExtID, Profile, RefID, User
@ -111,184 +111,26 @@ def facility_service() -> FlaskResponse:
return req.response(response)
'''
class CardStatus(object):
"""
List of statuses we return to the game for various reasons
"""
SUCCESS = 0
NO_PROFILE = 109
NOT_ALLOWED = 110
NOT_REGISTERED = 112
INVALID_PIN = 116
def create_refid(user_id: int) -> str:
with Database() as db:
# Create a new extid that is unique
while True:
e_id = random.randint(0, 89999999) + 10000000
if db.session.query(ExtID).filter(ExtID.uid == e_id).count() == 0:
break
# Use that ext_id
ext_id = ExtID(uid=e_id, game="GFDM", user_id=user_id)
try:
db.session.add(ext_id)
except Exception:
# Most likely a duplicate error as this user already has an ExtID for this
# game series
pass
# Create a new refid that is unique
while True:
r_id = "".join(random.choice("0123456789ABCDEF") for _ in range(16))
if db.session.query(RefID).filter(RefID.uid == r_id).count() == 0:
break
# Use that ref_id
ref_id = RefID(uid=r_id, game="GFDM", version=8, user_id=user_id)
db.session.add(ref_id)
db.session.commit()
uid = ref_id.uid
return uid
def has_profile(user_id: int) -> bool:
result = False
with Database() as db:
ref_id = (
db.session.query(RefID)
.filter(RefID.game == "GFDM", RefID.version == 8, RefID.user_id == user_id)
.one()
)
result = (
db.session.query(Profile).filter(Profile.ref_id == ref_id.uid).count() != 0
)
return result
@app.route("/cardmng/service", methods=["POST"])
@Services.route(ServiceType.CARDMNG)
def cardmng() -> Tuple[bytes, Dict[str, str]]:
"""
This is for dealing with the card management
"""
xml, model_str, module, method, command = eamuse_read_xml(request)
req = ServiceRequest(request)
if method == "inquire":
card_id = get_xml_attrib(xml[0], "cardid")
with Database() as db:
# Check if the user already exists
try:
card = db.session.query(Card).filter(Card.uid == card_id).one_or_none()
except MultipleResultsFound:
app.logger.error(f"Multiple Cards found for Card ID: {card_id}")
raise
if card is None:
# This user doesn't exist, force the system to create a new account
response = E.response(
E.cardmng({"status": str(CardStatus.NOT_REGISTERED)})
)
else:
# Special handing for looking up whether the previous game's profile
# existed
user = db.session.query(User).filter(User.uid == card.user_id).one()
bound = has_profile(user.uid)
expired = False
ref_id = (
db.session.query(RefID)
.filter(
RefID.game == "GFDM",
RefID.version == 8,
RefID.user_id == user.uid,
)
.one()
)
paseli_enabled = False
response = E.response(
E.cardmng(
{
"refid": ref_id.uid,
"dataid": ref_id.uid,
"newflag": "1", # A;ways seems to be set to 1
"binded": "1" if bound else "0",
"expired": "1" if expired else "0",
"ecflag": "1" if paseli_enabled else "0",
"useridflag": "1",
"extidflag": "1",
}
)
)
elif method == "getrefid":
# Given a card_id, and a pin, register the card with the system and generate a
# new data_id/ref_id + ext_id
card_id = get_xml_attrib(xml[0], "cardid")
pin = get_xml_attrib(xml[0], "passwd")
with Database() as db:
# Create the user object
user = User(pin=pin)
db.session.add(user)
# We must commit to assign a uid
db.session.commit()
user_id = user.uid
# Now insert the card, tying it to the account
card = Card(uid=card_id, user_id=user_id)
db.session.add(card)
db.session.commit()
ref_id = create_refid(user_id)
response = E.response(E.cardmng({"dataid": ref_id, "refid": ref_id}))
elif method == "authpass":
# Given a data_id/ref_id previously found via inquire, verify the pin
ref_id = get_xml_attrib(xml[0], "refid")
pin = get_xml_attrib(xml[0], "pass")
with Database() as db:
refid = db.session.query(RefID).filter(RefID.uid == ref_id).one()
user = (
db.session.query(User).filter(User.uid == refid.user_id).one_or_none()
)
if user is not None:
valid = pin == user.pin
else:
valid = False
response = E.response(
E.cardmng(
{
"status": str(
CardStatus.SUCCESS if valid else CardStatus.INVALID_PIN
)
}
)
)
elif method == "bindmodel":
# Given a refid, bind the user's card to the current version of the game
# TODO: Not implemented right now. do it later
ref_id = get_xml_attrib(xml[0], "refid")
response = E.response(E.cardmng({"dataid": ref_id}))
elif method == "getkeepspan":
# Unclear what this method does, return an arbitrary span
response = E.response(E.cardmng({"keepspan", "30"}))
elif method == "getdatalist":
# Unclear what this method does, return a dummy response
response = base_response(module)
if req.method == CardMng.INQUIRE:
response = CardMng.inquire(req)
elif req.method == CardMng.GETREFID:
response = CardMng.getrefid(req)
elif req.method == CardMng.AUTHPASS:
response = CardMng.authpass(req)
elif req.method == CardMng.BINDMODEL:
response = CardMng.bindmodel(req)
elif req.method == CardMng.GETKEEPSPAN:
response = CardMng.getkeepspan()
elif req.method == CardMng.GETDATALIST:
response = CardMng.getdatalist()
else:
response = base_response(module)
return eamuse_prepare_xml(response, request)
'''
raise Exception(f"Not sure how to handle this Cardmng Request: {req}")
return req.response(response)
@Services.route(ServiceType.LOCAL)
@ -301,6 +143,8 @@ def local_service() -> FlaskResponse:
response = Local.demodata(req)
elif req.module == Local.CARDUTIL:
response = Local.cardutil(req)
elif req.module == Local.GAMEINFO:
response = Local.gameinfo(req)
else:
raise Exception(f"Not sure how to handle this Local Request: {req}")
return req.response(response)