Compare commits
55 Commits
feature/ch
...
develop
Author | SHA1 | Date | |
---|---|---|---|
e11db14292 | |||
77152bf25c | |||
f346d8572d | |||
ce621065a4 | |||
2d84865155 | |||
10d38e14ae | |||
8194520cca | |||
2612fc984c | |||
dd546dcce2 | |||
21415de775 | |||
d16ebe27d9 | |||
5ec6cc0398 | |||
c92ede9e55 | |||
92422684ef | |||
3df0f3fb06 | |||
0c800759bb | |||
3ad56306bf | |||
d5c68a624f | |||
fa18b4c6a2 | |||
7e254a0281 | |||
abe480d007
|
|||
cccb5ce1a7 | |||
064f2b6b54 | |||
b62c89b749 | |||
46d79d156b | |||
fb4e10c2ae | |||
15e8eb535b | |||
392fdb3783 | |||
91545bb974 | |||
e52362d87f | |||
e3ec58b238 | |||
0a4dc8dbb0 | |||
180c027575 | |||
a86b8eeddb | |||
d72603d101 | |||
611806828a | |||
8b050e89eb | |||
394ec74fb7 | |||
a2a333b13f | |||
933d8bea21 | |||
f70af35343 | |||
1e7f367d0f | |||
134af15ed7 | |||
a4bcca9171 | |||
d598c8fba0 | |||
3b7a577ea2 | |||
dd10508e68 | |||
c0df7cd084 | |||
756c7ce951 | |||
4ceac7db35 | |||
f8888c2392 | |||
9bc18f179d | |||
eb66c9159f | |||
5c45091cec | |||
00d3b6e69a |
@ -435,7 +435,7 @@ class AllnetServlet:
|
||||
|
||||
else:
|
||||
machine = await self.data.arcade.get_machine(req.serial)
|
||||
if not machine or not machine['ota_enable'] or not machine['is_cab']:
|
||||
if not machine or not machine['ota_channel'] or not machine['is_cab']:
|
||||
resp = urllib.parse.unquote(urllib.parse.urlencode(vars(resp))) + "\n"
|
||||
if is_dfi:
|
||||
return PlainTextResponse(
|
||||
@ -446,15 +446,13 @@ class AllnetServlet:
|
||||
return PlainTextResponse(content=self.enc_lite(litekey, iv, resp))
|
||||
return PlainTextResponse(resp)
|
||||
|
||||
if path.exists(
|
||||
f"{self.config.allnet.update_cfg_folder}/{req.game_id}-{req.ver.replace('.', '')}-app.ini"
|
||||
):
|
||||
resp.uri = f"http://{self.config.server.hostname}:{self.config.server.port}/dl/ini/{req.game_id}-{req.ver.replace('.', '')}-app.ini"
|
||||
update = await self.data.arcade.get_ota_update(req.game_id, req.ver, machine['ota_channel'])
|
||||
if update:
|
||||
if update['app_ini'] and path.exists(f"{self.config.allnet.update_cfg_folder}/{update['app_ini']}"):
|
||||
resp.uri = f"http://{self.config.server.hostname}:{self.config.server.port}/dl/ini/{update['app_ini']}"
|
||||
|
||||
if path.exists(
|
||||
f"{self.config.allnet.update_cfg_folder}/{req.game_id}-{req.ver.replace('.', '')}-opt.ini"
|
||||
):
|
||||
resp.uri += f"|http://{self.config.server.hostname}:{self.config.server.port}/dl/ini/{req.game_id}-{req.ver.replace('.', '')}-opt.ini"
|
||||
if update['opt_ini'] and path.exists(f"{self.config.allnet.update_cfg_folder}/{update['opt_ini']}"):
|
||||
resp.uri += f"|http://{self.config.server.hostname}:{self.config.server.port}/dl/ini/{update['opt_ini']}"
|
||||
|
||||
if resp.uri:
|
||||
self.logger.info(f"Sending download uri {resp.uri}")
|
||||
@ -496,7 +494,7 @@ class AllnetServlet:
|
||||
f"{self.config.allnet.update_cfg_folder}/{req_file}", "r", encoding="utf-8"
|
||||
).read())
|
||||
|
||||
self.logger.info(f"DL INI File {req_file} not found")
|
||||
self.logger.warning(f"DL INI File {req_file} not found")
|
||||
return PlainTextResponse()
|
||||
|
||||
async def handle_dlorder_report(self, request: Request) -> bytes:
|
||||
@ -805,7 +803,8 @@ class BillingServlet:
|
||||
)
|
||||
|
||||
if req.traceleft > 0:
|
||||
self.logger.warning(f"{req.traceleft} unsent tracelogs")
|
||||
self.logger.info(f"Requesting 20 more of {req.traceleft} unsent tracelogs")
|
||||
return PlainTextResponse("result=6&waittime=0&linelimit=20\r\n")
|
||||
|
||||
playlimit = req.playlimit
|
||||
while req.playcnt > playlimit:
|
||||
@ -825,9 +824,6 @@ class BillingServlet:
|
||||
resp_str = urllib.parse.unquote(urllib.parse.urlencode(vars(resp))) + "\r\n"
|
||||
|
||||
self.logger.debug(f"response {vars(resp)}")
|
||||
if req.traceleft > 0: # TODO: should probably move this up so we don't do a ton of work that doesn't get used
|
||||
self.logger.info(f"Requesting 20 more of {req.traceleft} unsent tracelogs")
|
||||
return PlainTextResponse("result=6&waittime=0&linelimit=20\r\n")
|
||||
|
||||
return PlainTextResponse(resp_str)
|
||||
|
||||
|
@ -45,7 +45,7 @@ class ServerConfig:
|
||||
@property
|
||||
def ssl_cert(self) -> str:
|
||||
return CoreConfig.get_config_field(
|
||||
self.__config, "core", "title", "ssl_cert", default="cert/title.pem"
|
||||
self.__config, "core", "server", "ssl_cert", default="cert/title.pem"
|
||||
)
|
||||
|
||||
@property
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""CHUNITHM VERSE support
|
||||
|
||||
Revision ID: 49c295e89cd4
|
||||
Revises: f6007bbf057d
|
||||
Revises: 7070a6fa8cdc
|
||||
Create Date: 2025-03-09 14:10:03.067328
|
||||
|
||||
"""
|
||||
@ -13,7 +13,7 @@ from sqlalchemy.sql import func
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "49c295e89cd4"
|
||||
down_revision = "f6007bbf057d"
|
||||
down_revision = "7070a6fa8cdc"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
@ -60,7 +60,7 @@ def upgrade():
|
||||
sa.Column("conditionType", sa.Integer()),
|
||||
sa.Column("score", sa.Integer()),
|
||||
sa.Column("life", sa.Integer()),
|
||||
sa.Column("clearDate", sa.TIMESTAMP(), server_defaul=func.now()),
|
||||
sa.Column("clearDate", sa.TIMESTAMP(), server_default=func.now()),
|
||||
sa.UniqueConstraint(
|
||||
"version",
|
||||
"user",
|
||||
|
@ -10,6 +10,7 @@ import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
|
||||
revision = '5cf98cfe52ad'
|
||||
down_revision = '263884e774cc'
|
||||
branch_labels = None
|
||||
|
42
core/data/alembic/versions/7070a6fa8cdc_update_channels.py
Normal file
42
core/data/alembic/versions/7070a6fa8cdc_update_channels.py
Normal file
@ -0,0 +1,42 @@
|
||||
"""update_channels
|
||||
|
||||
Revision ID: 7070a6fa8cdc
|
||||
Revises: f6007bbf057d
|
||||
Create Date: 2025-09-27 16:09:55.853051
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import mysql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '7070a6fa8cdc'
|
||||
down_revision = 'f6007bbf057d'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('machine_update',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('game', sa.CHAR(length=4), nullable=False),
|
||||
sa.Column('version', sa.VARCHAR(length=15), nullable=False),
|
||||
sa.Column('channel', sa.VARCHAR(length=260), nullable=False),
|
||||
sa.Column('app_ini', sa.VARCHAR(length=260), nullable=True),
|
||||
sa.Column('opt_ini', sa.VARCHAR(length=260), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('game', 'version', 'channel', name='machine_update_uk'),
|
||||
mysql_charset='utf8mb4'
|
||||
)
|
||||
op.add_column('machine', sa.Column('ota_channel', sa.VARCHAR(length=260), nullable=True))
|
||||
op.drop_column('machine', 'ota_enable')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('machine', sa.Column('ota_enable', mysql.TINYINT(display_width=1), autoincrement=False, nullable=True))
|
||||
op.drop_column('machine', 'ota_channel')
|
||||
op.drop_table('machine_update')
|
||||
# ### end Alembic commands ###
|
@ -0,0 +1,29 @@
|
||||
"""Mai2 add PRiSM+ playlog support
|
||||
|
||||
Revision ID: bdf710616ba4
|
||||
Revises: 16f34bf7b968
|
||||
Create Date: 2025-04-02 12:42:08.981516
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'bdf710616ba4'
|
||||
|
||||
down_revision = '49c295e89cd4'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('mai2_playlog', sa.Column('extBool3', sa.Boolean(), nullable=True,server_default=sa.text("NULL")))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('mai2_playlog', 'extBool3')
|
||||
# ### end Alembic commands ###
|
@ -7,7 +7,7 @@ from sqlalchemy.dialects.mysql import insert
|
||||
from sqlalchemy.engine import Row
|
||||
from sqlalchemy.sql import func, select
|
||||
from sqlalchemy.sql.schema import ForeignKey, PrimaryKeyConstraint
|
||||
from sqlalchemy.types import JSON, Boolean, Integer, String, BIGINT, INTEGER, CHAR, FLOAT
|
||||
from sqlalchemy.types import JSON, Boolean, Integer, String, BIGINT, INTEGER, CHAR, FLOAT, VARCHAR
|
||||
|
||||
from core.data.schema.base import BaseData, metadata
|
||||
|
||||
@ -41,13 +41,26 @@ machine: Table = Table(
|
||||
Column("game", String(4)),
|
||||
Column("country", String(3)), # overwrites if not null
|
||||
Column("timezone", String(255)),
|
||||
Column("ota_enable", Boolean),
|
||||
Column("memo", String(255)),
|
||||
Column("is_cab", Boolean),
|
||||
Column("ota_channel", VARCHAR(260)),
|
||||
Column("data", JSON),
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
|
||||
update: Table = Table(
|
||||
"machine_update",
|
||||
metadata,
|
||||
Column("id", Integer, primary_key=True, nullable=False),
|
||||
Column("game", CHAR(4), nullable=False),
|
||||
Column("version", VARCHAR(15), nullable=False),
|
||||
Column("channel", VARCHAR(260), nullable=False),
|
||||
Column("app_ini", VARCHAR(260)),
|
||||
Column("opt_ini", VARCHAR(260)),
|
||||
UniqueConstraint("game", "version", "channel", name="machine_update_uk"),
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
|
||||
arcade_owner: Table = Table(
|
||||
"arcade_owner",
|
||||
metadata,
|
||||
@ -250,12 +263,12 @@ class ArcadeData(BaseData):
|
||||
return False
|
||||
return True
|
||||
|
||||
async def set_machine_can_ota(self, machine_id: int, can_ota: bool = False) -> bool:
|
||||
sql = machine.update(machine.c.id == machine_id).values(ota_enable = can_ota)
|
||||
async def set_machine_ota_channel(self, machine_id: int, channel_name: Optional[str] = None) -> bool:
|
||||
sql = machine.update(machine.c.id == machine_id).values(ota_channel = channel_name)
|
||||
|
||||
result = await self.execute(sql)
|
||||
if result is None:
|
||||
self.logger.error(f"Failed to update machine {machine_id} ota_enable to {can_ota}")
|
||||
self.logger.error(f"Failed to update machine {machine_id} ota channel to {channel_name}")
|
||||
return False
|
||||
return True
|
||||
|
||||
@ -530,6 +543,29 @@ class ArcadeData(BaseData):
|
||||
if result is not None:
|
||||
return result.fetchone()
|
||||
|
||||
async def create_ota_update(self, game_id: str, ver: str, channel: str, app: Optional[str], opt: Optional[str] = None) -> Optional[int]:
|
||||
result = await self.execute(insert(update).values(
|
||||
game = game_id,
|
||||
version = ver,
|
||||
channel = channel,
|
||||
app_ini = app,
|
||||
opt_ini = opt
|
||||
))
|
||||
|
||||
if result is None:
|
||||
self.logger.error(f"Failed to create {game_id} v{ver} update on channel {channel}")
|
||||
return result.lastrowid
|
||||
|
||||
async def get_ota_update(self, game_id: str, ver: str, channel: str) -> Optional[Row]:
|
||||
result = await self.execute(update.select(and_(
|
||||
and_(update.c.game == game_id, update.c.version == ver),
|
||||
update.c.channel == channel
|
||||
)))
|
||||
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchone()
|
||||
|
||||
def format_serial(
|
||||
self, platform_code: str, platform_rev: int, serial_letter: str, serial_num: int, append: int, dash: bool = False
|
||||
) -> str:
|
||||
|
@ -124,3 +124,15 @@ class UserData(BaseData):
|
||||
async def get_user_by_username(self, username: str) -> Optional[Row]:
|
||||
result = await self.execute(aime_user.select(aime_user.c.username == username))
|
||||
if result: return result.fetchone()
|
||||
|
||||
async def change_permission(self, user_id: int, new_perms: int) -> Optional[bool]:
|
||||
sql = aime_user.update(aime_user.c.id == user_id).values(permissions = new_perms)
|
||||
|
||||
result = await self.execute(sql)
|
||||
return result is not None
|
||||
|
||||
async def change_email(self, user_id: int, new_email: int) -> Optional[bool]:
|
||||
sql = aime_user.update(aime_user.c.id == user_id).values(email = new_email)
|
||||
|
||||
result = await self.execute(sql)
|
||||
return result is not None
|
||||
|
@ -1146,7 +1146,7 @@ class FE_Machine(FE_Base):
|
||||
new_country = frm.get('country', None)
|
||||
new_tz = frm.get('tz', None)
|
||||
new_is_cab = frm.get('is_cab', False) == 'on'
|
||||
new_is_ota = frm.get('is_ota', False) == 'on'
|
||||
new_ota_channel = frm.get('ota_channel', None)
|
||||
new_memo = frm.get('memo', None)
|
||||
|
||||
try:
|
||||
@ -1158,7 +1158,7 @@ class FE_Machine(FE_Base):
|
||||
did_country = await self.data.arcade.set_machine_country(cab['id'], new_country if new_country else None)
|
||||
did_timezone = await self.data.arcade.set_machine_timezone(cab['id'], new_tz if new_tz else None)
|
||||
did_real_cab = await self.data.arcade.set_machine_real_cabinet(cab['id'], new_is_cab)
|
||||
did_ota = await self.data.arcade.set_machine_can_ota(cab['id'], new_is_ota)
|
||||
did_ota = await self.data.arcade.set_machine_ota_channel(cab['id'], new_ota_channel if new_is_cab else None)
|
||||
did_memo = await self.data.arcade.set_machine_memo(cab['id'], new_memo if new_memo else None)
|
||||
|
||||
if not did_game or not did_country or not did_timezone or not did_real_cab or not did_ota or not did_memo:
|
||||
|
@ -3,13 +3,9 @@
|
||||
<script type="text/javascript">
|
||||
function swap_ota() {
|
||||
let is_cab = document.getElementById("is_cab").checked;
|
||||
let cbx_ota = document.getElementById("is_ota");
|
||||
let txt_ota = document.getElementById("ota_channel");
|
||||
|
||||
cbx_ota.disabled = !is_cab;
|
||||
|
||||
if (cbx_ota.disabled) {
|
||||
cbx_ota.checked = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<h1>Machine: {{machine.serial}}</h1>
|
||||
@ -64,8 +60,8 @@ Info
|
||||
<label for="is_cab" class="form-label">Real Cabinet</label>
|
||||
</div>
|
||||
<div class="col mb-3">
|
||||
<input type="checkbox" class="form-control-check" id="is_ota" name="is_ota" {{ 'checked' if machine.ota_enable else ''}}>
|
||||
<label for="is_ota" class="form-label">Allow OTA updates</label>
|
||||
<input type="text" class="form-control-check" id="ota_channel" name="ota_channel" value={{ machine.ota_channel }} {{ 'disabled' if not machine.is_cab else '' }}>
|
||||
<label for="ota_channel" class="form-label">OTA Update Channel</label>
|
||||
</div>
|
||||
<div class="col mb-3">
|
||||
</div>
|
||||
|
@ -149,41 +149,3 @@ class TitleServlet:
|
||||
self.logger.info(
|
||||
f"Serving {len(self.title_registry)} game codes {'on port ' + str(core_cfg.server.port) if core_cfg.server.port > 0 else ''}"
|
||||
)
|
||||
|
||||
def render_GET(self, request: Request, endpoints: dict) -> bytes:
|
||||
code = endpoints["title"]
|
||||
subaction = endpoints['subaction']
|
||||
|
||||
if code not in self.title_registry:
|
||||
self.logger.warning(f"Unknown game code {code}")
|
||||
request.setResponseCode(404)
|
||||
return b""
|
||||
|
||||
index = self.title_registry[code]
|
||||
handler = getattr(index, f"{subaction}", None)
|
||||
if handler is None:
|
||||
self.logger.error(f"{code} does not have handler for GET subaction {subaction}")
|
||||
request.setResponseCode(500)
|
||||
return b""
|
||||
|
||||
return handler(request, code, endpoints)
|
||||
|
||||
def render_POST(self, request: Request, endpoints: dict) -> bytes:
|
||||
code = endpoints["title"]
|
||||
subaction = endpoints['subaction']
|
||||
|
||||
if code not in self.title_registry:
|
||||
self.logger.warning(f"Unknown game code {code}")
|
||||
request.setResponseCode(404)
|
||||
return b""
|
||||
|
||||
index = self.title_registry[code]
|
||||
handler = getattr(index, f"{subaction}", None)
|
||||
if handler is None:
|
||||
self.logger.error(f"{code} does not have handler for POST subaction {subaction}")
|
||||
request.setResponseCode(500)
|
||||
return b""
|
||||
|
||||
endpoints.pop("title")
|
||||
endpoints.pop("subaction")
|
||||
return handler(request, code, endpoints)
|
||||
|
@ -205,7 +205,7 @@ Presents are items given to the user when they login, with a little animation (f
|
||||
### Versions
|
||||
|
||||
| Game Code | Version ID | Version Name |
|
||||
|-----------|------------|-------------------------|
|
||||
|----------|------------|-------------------------|
|
||||
| SBXL | 0 | maimai |
|
||||
| SBXL | 1 | maimai PLUS |
|
||||
| SBZF | 2 | maimai GreeN |
|
||||
@ -230,7 +230,7 @@ Presents are items given to the user when they login, with a little animation (f
|
||||
| SDEZ | 21 | maimai DX BUDDiES |
|
||||
| SDEZ | 22 | maimai DX BUDDiES PLUS |
|
||||
| SDEZ | 23 | maimai DX PRiSM |
|
||||
|
||||
| SDEZ | 24 | maimai DX PRiSM PLUS |
|
||||
|
||||
### Importer
|
||||
|
||||
@ -284,6 +284,18 @@ crypto:
|
||||
"23_chn": ["0000000000000000000000000000000000000000000000000000000000000000", "00000000000000000000000000000000", "0000000000000000"]
|
||||
```
|
||||
|
||||
| Option | Info |
|
||||
|-----------------|------------------------------------------------------|
|
||||
| `chart_deliver` | This option is used to delivery charts to the client |
|
||||
|
||||
If you would like to use chart delivery, set this option to `True` and configure the directory to read from. Then put charts in your chart folder like this:
|
||||
```
|
||||
chart_folder/23/music001736/001736_00.ma2
|
||||
chart_folder/23/music001736/001736_01.ma2 # PRiSM
|
||||
chart_folder/24/music001901/001901_00.ma2
|
||||
chart_folder/24/music001901/001901_01.ma2 # PRiSM PLUS
|
||||
```
|
||||
|
||||
## Hatsune Miku Project Diva
|
||||
|
||||
### SBZV
|
||||
|
@ -83,6 +83,7 @@ Games listed below have been tested and confirmed working. Only game versions ol
|
||||
+ BUDDiES
|
||||
+ BUDDiES PLUS
|
||||
+ PRiSM
|
||||
+ PRiSM PLUS
|
||||
|
||||
+ O.N.G.E.K.I.
|
||||
+ SUMMER
|
||||
|
@ -208,7 +208,8 @@ class CardMakerReader(BaseReader):
|
||||
"1.35": Mai2Constants.VER_MAIMAI_DX_FESTIVAL_PLUS,
|
||||
"1.40": Mai2Constants.VER_MAIMAI_DX_BUDDIES,
|
||||
"1.45": Mai2Constants.VER_MAIMAI_DX_BUDDIES_PLUS,
|
||||
"1.50": Mai2Constants.VER_MAIMAI_DX_PRISM
|
||||
"1.50": Mai2Constants.VER_MAIMAI_DX_PRISM,
|
||||
"1.55": Mai2Constants.VER_MAIMAI_DX_PRISM_PLUS
|
||||
}
|
||||
|
||||
for root, dirs, files in os.walk(base_dir):
|
||||
|
@ -100,7 +100,7 @@ class DivaServlet(BaseServlet):
|
||||
|
||||
try:
|
||||
handler = getattr(self.base, f"handle_{bin_req_data['cmd']}_request")
|
||||
resp = handler(bin_req_data)
|
||||
resp = await handler(bin_req_data)
|
||||
|
||||
except AttributeError as e:
|
||||
self.logger.warning(f"Unhandled {bin_req_data['cmd']} request {e}")
|
||||
|
@ -162,8 +162,8 @@ class IDACServlet(BaseServlet):
|
||||
resp = {
|
||||
"status_code": "0",
|
||||
# Only IPv4 is supported
|
||||
"host": self.game_config.server.matching_host,
|
||||
"port": self.game_config.server.matching_p2p,
|
||||
"host": self.game_cfg.server.matching_host,
|
||||
"port": self.game_cfg.server.matching_p2p,
|
||||
"room_name": "INDTA",
|
||||
"state": 1,
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ class IDZServlet(BaseServlet):
|
||||
def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None:
|
||||
super().__init__(core_cfg, cfg_dir)
|
||||
self.game_cfg = IDZConfig()
|
||||
self.rsa_keys: List[IDZKey] = []
|
||||
if path.exists(f"{cfg_dir}/{IDZConstants.CONFIG_NAME}"):
|
||||
self.game_cfg.update(
|
||||
yaml.safe_load(open(f"{cfg_dir}/{IDZConstants.CONFIG_NAME}"))
|
||||
@ -38,8 +39,6 @@ class IDZServlet(BaseServlet):
|
||||
backupCount=10,
|
||||
)
|
||||
|
||||
self.rsa_keys: List[IDZKey] = []
|
||||
|
||||
fileHandler.setFormatter(log_fmt)
|
||||
|
||||
consoleHandler = logging.StreamHandler()
|
||||
@ -79,7 +78,32 @@ class IDZServlet(BaseServlet):
|
||||
return False
|
||||
|
||||
if len(game_cfg.rsa_keys) <= 0 or not game_cfg.server.aes_key:
|
||||
logging.getLogger("idz").error("IDZ: No RSA/AES keys! IDZ cannot start")
|
||||
logger = logging.getLogger("idz")
|
||||
if not hasattr(logger, "inited"):
|
||||
log_fmt_str = "[%(asctime)s] IDZ | %(levelname)s | %(message)s"
|
||||
log_fmt = logging.Formatter(log_fmt_str)
|
||||
fileHandler = TimedRotatingFileHandler(
|
||||
"{0}/{1}.log".format(core_cfg.server.log_dir, "idz"),
|
||||
encoding="utf8",
|
||||
when="d",
|
||||
backupCount=10,
|
||||
)
|
||||
|
||||
fileHandler.setFormatter(log_fmt)
|
||||
|
||||
consoleHandler = logging.StreamHandler()
|
||||
consoleHandler.setFormatter(log_fmt)
|
||||
|
||||
logger.addHandler(fileHandler)
|
||||
logger.addHandler(consoleHandler)
|
||||
|
||||
logger.setLevel(game_cfg.server.loglevel)
|
||||
coloredlogs.install(
|
||||
level=game_cfg.server.loglevel, logger=logger, fmt=log_fmt_str
|
||||
)
|
||||
logger.inited = True
|
||||
|
||||
logger.error("No RSA/AES keys! IDZ cannot start")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
@ -53,8 +53,10 @@ class IDZUserDB:
|
||||
|
||||
async def connection_cb(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
|
||||
self.logger.debug(f"Connection made from {writer.get_extra_info('peername')[0]}")
|
||||
sent_handshake = False
|
||||
while True:
|
||||
try:
|
||||
if not sent_handshake:
|
||||
base = 0
|
||||
|
||||
for i in range(len(self.static_key) - 1):
|
||||
@ -75,6 +77,7 @@ class IDZUserDB:
|
||||
|
||||
writer.write(result)
|
||||
await writer.drain()
|
||||
sent_handshake = True
|
||||
|
||||
data: bytes = await reader.read(4096)
|
||||
if len(data) == 0:
|
||||
@ -88,7 +91,7 @@ class IDZUserDB:
|
||||
self.logger.debug("Connection reset, disconnecting")
|
||||
return
|
||||
|
||||
def dataReceived(self, data: bytes, reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
|
||||
async def dataReceived(self, data: bytes, reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
|
||||
self.logger.debug(f"Receive data {data.hex()}")
|
||||
client_ip = writer.get_extra_info('peername')[0]
|
||||
crypt = AES.new(self.static_key, AES.MODE_ECB)
|
||||
|
@ -61,6 +61,7 @@ class Mai2Constants:
|
||||
VER_MAIMAI_DX_BUDDIES = 21
|
||||
VER_MAIMAI_DX_BUDDIES_PLUS = 22
|
||||
VER_MAIMAI_DX_PRISM = 23
|
||||
VER_MAIMAI_DX_PRISM_PLUS = 24
|
||||
|
||||
VERSION_STRING = (
|
||||
"maimai",
|
||||
@ -86,7 +87,8 @@ class Mai2Constants:
|
||||
"maimai DX FESTiVAL PLUS",
|
||||
"maimai DX BUDDiES",
|
||||
"maimai DX BUDDiES PLUS",
|
||||
"maimai DX PRiSM"
|
||||
"maimai DX PRiSM",
|
||||
"maimai DX PRiSM PLUS"
|
||||
)
|
||||
KALEIDXSCOPE_KEY_CONDITION={
|
||||
1: [11009, 11008, 11100, 11097, 11098, 11099, 11163, 11162, 11161, 11228, 11229, 11231, 11463, 11464, 11465, 11538, 11539, 11541, 11620, 11622, 11623, 11737, 11738, 11164, 11230, 11466, 11540, 11621, 11739],
|
||||
@ -94,9 +96,21 @@ class Mai2Constants:
|
||||
2: [11102, 11234, 11300, 11529, 11542, 11612],
|
||||
#白の扉: set Frame as "Latent Kingdom" (459504), play 3 or 4 songs by the composer 大国奏音 in 1 pc
|
||||
3: [],
|
||||
#紫の扉: need to enter redeem code 51090942171709440000
|
||||
#紫の扉: JP: need to enter redeem code 51090942171709440000
|
||||
4: [11023, 11106, 11221, 11222, 11300, 11374, 11458, 11523, 11619, 11663, 11746],
|
||||
#青の扉: Played 11 songs
|
||||
#黑の扉: Played 11 songs
|
||||
5: [11003, 11095, 11152, 11224, 11296, 11375, 11452, 11529, 11608, 11669, 11736, 11806],
|
||||
#黄の扉: Use random selection to play one of the songs
|
||||
6: [212, 213, 337, 270, 271, 11504, 339, 453, 11336, 11852],
|
||||
#赤の扉: Played 10 songs
|
||||
7: [],
|
||||
#PRISM TOWER: Get the key after clearing six doors.
|
||||
8: [],
|
||||
#KALEIDXSCOPE_FIRST_STAGE: Clear Prism Tower
|
||||
9: [],
|
||||
#希望の扉: CLEAR KALEIDXSCOPE_FIRST_STAGE
|
||||
10: []
|
||||
#KALEIDXSCOPE_SECOND_STAGE: JP: scan the DXPASS of 希望の鍵, will automatically unlock after clearing 希望の扉 in artemis
|
||||
}
|
||||
MAI_VERSION_LUT = {
|
||||
"100": VER_MAIMAI,
|
||||
@ -125,7 +139,8 @@ class Mai2Constants:
|
||||
"135": VER_MAIMAI_DX_FESTIVAL_PLUS,
|
||||
"140": VER_MAIMAI_DX_BUDDIES,
|
||||
"145": VER_MAIMAI_DX_BUDDIES_PLUS,
|
||||
"150": VER_MAIMAI_DX_PRISM
|
||||
"150": VER_MAIMAI_DX_PRISM,
|
||||
"155": VER_MAIMAI_DX_PRISM_PLUS
|
||||
}
|
||||
|
||||
@classmethod
|
||||
|
@ -32,6 +32,7 @@ from .festivalplus import Mai2FestivalPlus
|
||||
from .buddies import Mai2Buddies
|
||||
from .buddiesplus import Mai2BuddiesPlus
|
||||
from .prism import Mai2Prism
|
||||
from .prismplus import Mai2PrismPlus
|
||||
|
||||
|
||||
class Mai2Servlet(BaseServlet):
|
||||
@ -68,7 +69,8 @@ class Mai2Servlet(BaseServlet):
|
||||
Mai2FestivalPlus,
|
||||
Mai2Buddies,
|
||||
Mai2BuddiesPlus,
|
||||
Mai2Prism
|
||||
Mai2Prism,
|
||||
Mai2PrismPlus
|
||||
]
|
||||
|
||||
self.logger = logging.getLogger("mai2")
|
||||
@ -345,8 +347,10 @@ class Mai2Servlet(BaseServlet):
|
||||
internal_ver = Mai2Constants.VER_MAIMAI_DX_BUDDIES
|
||||
elif version >= 145 and version < 150: # BUDDiES PLUS
|
||||
internal_ver = Mai2Constants.VER_MAIMAI_DX_BUDDIES_PLUS
|
||||
elif version >= 150: # PRiSM
|
||||
elif version >= 150 and version < 155:
|
||||
internal_ver = Mai2Constants.VER_MAIMAI_DX_PRISM
|
||||
elif version >= 155:
|
||||
internal_ver = Mai2Constants.VER_MAIMAI_DX_PRISM_PLUS
|
||||
|
||||
elif game_code == "SDGA": # Int
|
||||
if version < 105: # 1.0
|
||||
@ -369,8 +373,10 @@ class Mai2Servlet(BaseServlet):
|
||||
internal_ver = Mai2Constants.VER_MAIMAI_DX_BUDDIES
|
||||
elif version >= 145 and version < 150: # BUDDiES PLUS
|
||||
internal_ver = Mai2Constants.VER_MAIMAI_DX_BUDDIES_PLUS
|
||||
elif version >= 150: # PRiSM
|
||||
elif version >= 150 and version < 155:
|
||||
internal_ver = Mai2Constants.VER_MAIMAI_DX_PRISM
|
||||
elif version >= 155:
|
||||
internal_ver = Mai2Constants.VER_MAIMAI_DX_PRISM_PLUS
|
||||
|
||||
elif game_code == "SDGB": # Chn
|
||||
if version < 110: # Muji
|
||||
@ -386,6 +392,7 @@ class Mai2Servlet(BaseServlet):
|
||||
elif version >= 150: # PRiSM
|
||||
internal_ver = Mai2Constants.VER_MAIMAI_DX_PRISM
|
||||
|
||||
|
||||
if all(c in string.hexdigits for c in endpoint) and len(endpoint) == 32:
|
||||
|
||||
if game_code == "SDGA":
|
||||
|
@ -34,6 +34,7 @@ class Mai2Prism(Mai2BuddiesPlus):
|
||||
|
||||
padded_music_id = str(data["musicId"]).zfill(6)
|
||||
padded_level_id = str(data["level"]).zfill(2)
|
||||
music_folder = f"music{padded_music_id}"
|
||||
|
||||
if data["type"] == 0:
|
||||
target_filename = f"{padded_music_id}_{padded_level_id}.ma2"
|
||||
@ -42,11 +43,11 @@ class Mai2Prism(Mai2BuddiesPlus):
|
||||
elif data["type"] == 2:
|
||||
target_filename = f"{padded_music_id}_{padded_level_id}_R.ma2"
|
||||
else:
|
||||
self.logger.error("Valid MusicScore type!")
|
||||
self.logger.error("Invalid MusicScore type!")
|
||||
return {"gameMusicScore": {"musicId": data["musicId"], "level": data["level"], "type": data["type"], "scoreData": ""}}
|
||||
|
||||
|
||||
chart_path = os.path.join(self.game_config.charts.chart_folder, target_filename)
|
||||
chart_path = os.path.join(self.game_config.charts.chart_folder, str(self.version), music_folder, target_filename)
|
||||
if os.path.isfile(chart_path):
|
||||
with open(chart_path, 'rb') as file:
|
||||
file_content = file.read()
|
||||
@ -60,7 +61,7 @@ class Mai2Prism(Mai2BuddiesPlus):
|
||||
}
|
||||
}
|
||||
else:
|
||||
self.logger.warning(f"Chart {target_filename} not found!")
|
||||
self.logger.warning(f"Version {self.version} Chart {target_filename} not found!")
|
||||
return {"gameMusicScore": {"musicId": data["musicId"], "level": data["level"], "type": data["type"], "scoreData": ""}}
|
||||
|
||||
|
||||
|
141
titles/mai2/prismplus.py
Normal file
141
titles/mai2/prismplus.py
Normal file
@ -0,0 +1,141 @@
|
||||
from typing import Dict
|
||||
|
||||
from core.config import CoreConfig
|
||||
from titles.mai2.prism import Mai2Prism
|
||||
from titles.mai2.const import Mai2Constants
|
||||
from titles.mai2.config import Mai2Config
|
||||
|
||||
|
||||
|
||||
class Mai2PrismPlus(Mai2Prism):
|
||||
def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None:
|
||||
super().__init__(cfg, game_cfg)
|
||||
self.version = Mai2Constants.VER_MAIMAI_DX_PRISM_PLUS
|
||||
|
||||
async def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict:
|
||||
user_data = await super().handle_cm_get_user_preview_api_request(data)
|
||||
|
||||
# hardcode lastDataVersion for CardMaker
|
||||
user_data["lastDataVersion"] = "1.55.00"
|
||||
return user_data
|
||||
|
||||
async def handle_upsert_client_play_time_api_request(self, data: Dict) -> Dict:
|
||||
return{
|
||||
"returnCode": 1,
|
||||
"apiName": "UpsertClientPlayTimeApi"
|
||||
}
|
||||
async def handle_get_game_kaleidx_scope_api_request(self, data: Dict) -> Dict:
|
||||
return {
|
||||
"gameKaleidxScopeList": [
|
||||
{"gateId": 1, "phaseId": 6},
|
||||
{"gateId": 2, "phaseId": 6},
|
||||
{"gateId": 3, "phaseId": 6},
|
||||
{"gateId": 4, "phaseId": 6},
|
||||
{"gateId": 5, "phaseId": 6},
|
||||
{"gateId": 6, "phaseId": 6},
|
||||
{"gateId": 7, "phaseId": 6},
|
||||
{"gateId": 8, "phaseId": 6},
|
||||
{"gateId": 9, "phaseId": 6},
|
||||
{"gateId": 10, "phaseId": 13}
|
||||
]
|
||||
}
|
||||
|
||||
async def handle_get_user_kaleidx_scope_api_request(self, data: Dict) -> Dict:
|
||||
# kaleidxscope keyget condition judgement
|
||||
# player may get key before GateFound
|
||||
for gate in range(1,11):
|
||||
if gate == 1 or gate == 4 or gate == 6:
|
||||
condition_satisfy = 0
|
||||
for condition in Mai2Constants.KALEIDXSCOPE_KEY_CONDITION[gate]:
|
||||
score_list = await self.data.score.get_best_scores(user_id=data["userId"], song_id=condition)
|
||||
if score_list:
|
||||
condition_satisfy = condition_satisfy + 1
|
||||
if len(Mai2Constants.KALEIDXSCOPE_KEY_CONDITION[gate]) == condition_satisfy:
|
||||
new_kaleidxscope = {'gateId': gate, "isKeyFound": True}
|
||||
await self.data.score.put_user_kaleidxscope(data["userId"], new_kaleidxscope)
|
||||
|
||||
elif gate == 2:
|
||||
user_profile = await self.data.profile.get_profile_detail(user_id=data["userId"], version=self.version)
|
||||
user_frame = user_profile["frameId"]
|
||||
if user_frame == 459504:
|
||||
playlogs = await self.data.score.get_playlogs(user_id=data["userId"], idx=0, limit=0)
|
||||
|
||||
playlog_dict = {}
|
||||
for playlog in playlogs:
|
||||
playlog_id = playlog["playlogId"]
|
||||
if playlog_id not in playlog_dict:
|
||||
playlog_dict[playlog_id] = []
|
||||
playlog_dict[playlog_id].append(playlog["musicId"])
|
||||
valid_playlogs = []
|
||||
allowed_music = set(Mai2Constants.KALEIDXSCOPE_KEY_CONDITION[2])
|
||||
for playlog_id, music_ids in playlog_dict.items():
|
||||
|
||||
if len(music_ids) != len(set(music_ids)):
|
||||
continue
|
||||
all_valid = True
|
||||
for mid in music_ids:
|
||||
if mid not in allowed_music:
|
||||
all_valid = False
|
||||
break
|
||||
if all_valid:
|
||||
valid_playlogs.append(playlog_id)
|
||||
|
||||
if valid_playlogs:
|
||||
new_kaleidxscope = {'gateId': 2, "isKeyFound": True}
|
||||
await self.data.score.put_user_kaleidxscope(data["userId"], new_kaleidxscope)
|
||||
|
||||
|
||||
elif gate == 5:
|
||||
|
||||
playlogs = await self.data.score.get_playlogs(user_id=data["userId"], idx=0, limit=0)
|
||||
allowed_music = set(Mai2Constants.KALEIDXSCOPE_KEY_CONDITION[5])
|
||||
valid_playlogs = []
|
||||
|
||||
for playlog in playlogs:
|
||||
if playlog["extBool2"] == 1 and playlog["musicId"] in allowed_music:
|
||||
valid_playlogs.append(playlog["playlogId"]) # 直接记录 playlogId
|
||||
if valid_playlogs:
|
||||
new_kaleidxscope = {'gateId': 5, "isKeyFound": True}
|
||||
await self.data.score.put_user_kaleidxscope(data["userId"], new_kaleidxscope)
|
||||
|
||||
elif gate == 7:
|
||||
|
||||
played_kaleidxscope_list = await self.data.score.get_user_kaleidxscope_list(data["userId"])
|
||||
check_results = {}
|
||||
for i in range(1,7):
|
||||
check_results[i] = False
|
||||
for played_kaleidxscope in played_kaleidxscope_list:
|
||||
if played_kaleidxscope[2] == i and played_kaleidxscope[5] == True:
|
||||
check_results[i] = True
|
||||
break
|
||||
all_true = all(check_results.values())
|
||||
|
||||
if all_true:
|
||||
new_kaleidxscope = {'gateId': 7, "isKeyFound": True}
|
||||
await self.data.score.put_user_kaleidxscope(data["userId"], new_kaleidxscope)
|
||||
|
||||
elif gate == 10:
|
||||
|
||||
played_kaleidxscope_list = await self.data.score.get_user_kaleidxscope_list(data["userId"])
|
||||
for played_kaleidxscope in played_kaleidxscope_list:
|
||||
if played_kaleidxscope[2] == 9 and played_kaleidxscope[5] == True:
|
||||
new_kaleidxscope = {'gateId': 10, "isGateFound": True, "isKeyFound": True}
|
||||
await self.data.score.put_user_kaleidxscope(data["userId"], new_kaleidxscope)
|
||||
|
||||
|
||||
|
||||
kaleidxscope = await self.data.score.get_user_kaleidxscope_list(data["userId"])
|
||||
|
||||
if kaleidxscope is None:
|
||||
return {"userId": data["userId"], "userKaleidxScopeList":[]}
|
||||
|
||||
kaleidxscope_list = []
|
||||
for kaleidxscope_data in kaleidxscope:
|
||||
tmp = kaleidxscope_data._asdict()
|
||||
tmp.pop("user")
|
||||
tmp.pop("id")
|
||||
kaleidxscope_list.append(tmp)
|
||||
return {
|
||||
"userId": data["userId"],
|
||||
"userKaleidxScopeList": kaleidxscope_list
|
||||
}
|
@ -148,7 +148,8 @@ playlog = Table(
|
||||
Column("extNum2", Integer),
|
||||
Column("extNum4", Integer),
|
||||
Column("extBool1", Boolean), # new with buddies
|
||||
Column("extBool2", Boolean), # new with prism
|
||||
Column("extBool2", Boolean), # new with prism IsRandomSelect
|
||||
Column("extBool3", Boolean), # new with prism+ IsTrackSkip
|
||||
Column("trialPlayAchievement", Integer),
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
|
@ -604,13 +604,6 @@ class OngekiStaticData(BaseData):
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
||||
async def get_opts(self) -> Optional[List[Row]]:
|
||||
result = await self.execute(opts.select())
|
||||
|
||||
if result is None:
|
||||
return None
|
||||
return result.fetchall()
|
||||
|
||||
async def set_opt_enabled(self, opt_id: int, enabled: bool) -> bool:
|
||||
result = await self.execute(opts.update(opts.c.id == opt_id).values(isEnable=enabled))
|
||||
|
||||
|
@ -152,7 +152,7 @@ class SaoServlet(BaseServlet):
|
||||
|
||||
else:
|
||||
self.logger.error(f"Unknown response type {type(resp)}")
|
||||
return SaoNoopResponse(req_header.cmd + 1).make()
|
||||
return Response(SaoNoopResponse(req_header.cmd + 1).make())
|
||||
|
||||
self.logger.debug(f"Response: {resp.hex()}")
|
||||
|
||||
|
593
tui.py
593
tui.py
@ -8,9 +8,11 @@ import bcrypt
|
||||
import secrets
|
||||
import string
|
||||
from sqlalchemy.engine import Row
|
||||
import inflection
|
||||
|
||||
from core.data import Data
|
||||
from core.config import CoreConfig
|
||||
from core.const import AllnetCountryCode
|
||||
|
||||
try:
|
||||
from asciimatics.widgets import Frame, Layout, Text, Button, RadioButtons, CheckBox, Divider, Label
|
||||
@ -77,10 +79,10 @@ class State:
|
||||
return self.id if self.id else 0
|
||||
|
||||
def __init__(self):
|
||||
self.selected_user: self.SelectedUser = self.SelectedUser()
|
||||
self.selected_card: self.SelectedCard = self.SelectedCard()
|
||||
self.selected_arcade: self.SelectedArcade = self.SelectedArcade()
|
||||
self.selected_machine: self.SelectedMachine = self.SelectedMachine()
|
||||
self.selected_user = self.SelectedUser()
|
||||
self.selected_card = self.SelectedCard()
|
||||
self.selected_arcade = self.SelectedArcade()
|
||||
self.selected_machine = self.SelectedMachine()
|
||||
self.last_err: str = ""
|
||||
self.search_results: List[Row] = []
|
||||
self.search_type: str = ""
|
||||
@ -89,6 +91,7 @@ class State:
|
||||
self.selected_user = self.SelectedUser(id, username)
|
||||
|
||||
def clear_user(self) -> None:
|
||||
print(self.selected_user)
|
||||
self.selected_user = self.SelectedUser()
|
||||
|
||||
def set_card(self, id: int, access_code: Optional[str]) -> None:
|
||||
@ -138,7 +141,7 @@ class MainView(Frame):
|
||||
layout.add_widget(Button("User Management", self._user_mgmt))
|
||||
layout.add_widget(Button("Card Management", self._card_mgmt))
|
||||
layout.add_widget(Button("Arcade Management", self._arcade_mgmt))
|
||||
layout.add_widget(Button("Machine Management", self._mech_mgmt))
|
||||
layout.add_widget(Button("Machine Management", self._machine_mgmt))
|
||||
layout.add_widget(Button("Quit", self._quit))
|
||||
|
||||
self.fix()
|
||||
@ -155,17 +158,17 @@ class MainView(Frame):
|
||||
self.save()
|
||||
raise NextScene("Arcade Management")
|
||||
|
||||
def _mech_mgmt(self):
|
||||
def _machine_mgmt(self):
|
||||
self.save()
|
||||
raise NextScene("Mech Management")
|
||||
raise NextScene("Machine Management")
|
||||
|
||||
@staticmethod
|
||||
def _quit():
|
||||
raise StopApplication("User pressed quit")
|
||||
|
||||
class ManageUser(Frame):
|
||||
class ManageUserView(Frame):
|
||||
def __init__(self, screen: Screen):
|
||||
super(ManageUser, self).__init__(
|
||||
super(ManageUserView, self).__init__(
|
||||
screen,
|
||||
screen.height * 2 // 3,
|
||||
screen.width * 2 // 3,
|
||||
@ -192,7 +195,7 @@ class ManageUser(Frame):
|
||||
usr_cards = []
|
||||
if state.selected_user.id != 0:
|
||||
cards = loop.run_until_complete(data.card.get_user_cards(state.selected_user.id))
|
||||
for card in cards:
|
||||
for card in cards or []:
|
||||
usr_cards.append(card._asdict())
|
||||
|
||||
if len(usr_cards) > 0:
|
||||
@ -250,9 +253,9 @@ class ManageUser(Frame):
|
||||
self.save()
|
||||
raise NextScene("Main")
|
||||
|
||||
class ManageCard(Frame):
|
||||
class ManageCardView(Frame):
|
||||
def __init__(self, screen: Screen):
|
||||
super(ManageCard, self).__init__(
|
||||
super(ManageCardView, self).__init__(
|
||||
screen,
|
||||
screen.height * 2 // 3,
|
||||
screen.width * 2 // 3,
|
||||
@ -416,6 +419,19 @@ class SearchResultsView(Frame):
|
||||
|
||||
opts.append((f"{usr['id']:05d} | {name} | {usr['permissions']:08b} | {usr['email']}", state.SelectedUser(usr["id"], str(usr['username']))))
|
||||
|
||||
elif state.search_type == "arcade":
|
||||
layout.add_widget(Label(" ID | Name | Country | # Machines "))
|
||||
layout.add_widget(Divider())
|
||||
|
||||
for ac in state.search_results:
|
||||
name = str(ac['name'])
|
||||
if len(name) < 8:
|
||||
name = str(ac['name']) + ' ' * (8 - len(name))
|
||||
elif len(name) > 8:
|
||||
name = ac['name'][:5] + "..."
|
||||
|
||||
opts.append((f"{ac['id']:04X} | {name} | {ac['country']} | {usr['mech_ct']}", state.SelectedArcade(ac["id"], ac['country'], str(ac['name']))))
|
||||
|
||||
layout.add_widget(RadioButtons(opts, "", "selopt"))
|
||||
|
||||
self.fix()
|
||||
@ -423,13 +439,26 @@ class SearchResultsView(Frame):
|
||||
def _select_current(self):
|
||||
self.save()
|
||||
a = self.data.get('selopt')
|
||||
if state.search_type == "user":
|
||||
state.set_user(a.id, a.name)
|
||||
raise NextScene("User Management")
|
||||
|
||||
elif state.search_type == "arcade":
|
||||
state.set_arcade(a.id, a.country, a.name)
|
||||
raise NextScene("Arcade Management")
|
||||
|
||||
def _cancel(self):
|
||||
state.clear_last_err()
|
||||
if state.search_type == "user":
|
||||
raise NextScene("User Management")
|
||||
|
||||
elif state.search_type == "arcade":
|
||||
raise NextScene("Arcade Management")
|
||||
|
||||
def _back(self):
|
||||
self.save()
|
||||
raise NextScene("Main")
|
||||
|
||||
class LookupUserView(Frame):
|
||||
def __init__(self, screen):
|
||||
super(LookupUserView, self).__init__(
|
||||
@ -528,14 +557,550 @@ class LookupUserView(Frame):
|
||||
self.find_widget('status').value = state.last_err
|
||||
raise NextScene("User Management")
|
||||
|
||||
class EditUserView(Frame):
|
||||
def __init__(self, screen):
|
||||
super(EditUserView, self).__init__(
|
||||
screen,
|
||||
screen.height * 2 // 3,
|
||||
screen.width * 2 // 3,
|
||||
hover_focus=True,
|
||||
can_scroll=False,
|
||||
title="Edit User",
|
||||
on_load=self._redraw
|
||||
)
|
||||
|
||||
layout = Layout([100], fill_frame=True)
|
||||
self.add_layout(layout)
|
||||
layout.add_widget(Text("Username:", "username"))
|
||||
layout.add_widget(Text("Email:", "email"))
|
||||
layout.add_widget(Text("Password:", "passwd"))
|
||||
layout.add_widget(RadioButtons([
|
||||
("User", "1"),
|
||||
("User Manager", "2"),
|
||||
("Arcde Manager", "4"),
|
||||
("Sysadmin", "8"),
|
||||
("Owner", "255"),
|
||||
], "Role:", "role"))
|
||||
|
||||
layout3 = Layout([100])
|
||||
self.add_layout(layout3)
|
||||
layout3.add_widget(Text("", f"status", readonly=True, disabled=True))
|
||||
|
||||
layout2 = Layout([1, 1, 1, 1])
|
||||
self.add_layout(layout2)
|
||||
layout2.add_widget(Button("Save", self._ok), 0)
|
||||
layout2.add_widget(Button("Cancel", self._cancel), 3)
|
||||
|
||||
self.fix()
|
||||
|
||||
def _redraw(self):
|
||||
uinfo = loop.run_until_complete(data.user.get_user(state.selected_user.id))
|
||||
self.find_widget('username').value = uinfo['username']
|
||||
self.find_widget('email').value = uinfo['email']
|
||||
self.find_widget('role').value = str(uinfo['permissions'])
|
||||
|
||||
def _ok(self):
|
||||
self.save()
|
||||
if not self.data.get("username"):
|
||||
state.set_last_err("Username cannot be blank")
|
||||
self.find_widget('status').value = state.last_err
|
||||
self.screen.reset()
|
||||
return
|
||||
|
||||
state.clear_last_err()
|
||||
self.find_widget('status').value = state.last_err
|
||||
|
||||
pw = self.data.get("passwd")
|
||||
hash = bcrypt.hashpw(pw.encode(), bcrypt.gensalt())
|
||||
|
||||
is_good = loop.run_until_complete(self._update_user_async(self.data.get("username"), hash.decode(), self.data.get("email"), self.data.get('role')))
|
||||
|
||||
self.find_widget('status').value = "User Updated" if is_good else "User update failed"
|
||||
|
||||
raise NextScene("User Management")
|
||||
|
||||
async def _update_user_async(self, username: Optional[str], password: Optional[str], email: Optional[str], role: Optional[str]) -> bool:
|
||||
if username: namechange_ok = await data.user.change_username(state.selected_user.id, username)
|
||||
else: namechange_ok = True
|
||||
|
||||
if password: pw_ok = await data.user.change_password(state.selected_user.id, password)
|
||||
else: pw_ok = True
|
||||
|
||||
if role: role_ok = await data.user.change_permission(state.selected_user.id, role)
|
||||
else: role_ok = True
|
||||
|
||||
if email: email_ok = await data.user.change_email(state.selected_user.id, email)
|
||||
else: email_ok = True
|
||||
|
||||
state.set_user(state.selected_user.id, username if username and namechange_ok else state.selected_user.name)
|
||||
return namechange_ok and pw_ok and role_ok and email_ok
|
||||
|
||||
def _cancel(self):
|
||||
state.clear_last_err()
|
||||
self.find_widget('status').value = state.last_err
|
||||
raise NextScene("User Management")
|
||||
|
||||
class ManageArcadeView(Frame):
|
||||
def __init__(self, screen: Screen):
|
||||
super(ManageArcadeView, self).__init__(
|
||||
screen,
|
||||
screen.height * 2 // 3,
|
||||
screen.width * 2 // 3,
|
||||
hover_focus=True,
|
||||
can_scroll=False,
|
||||
title="Arcade Management",
|
||||
on_load=self._redraw
|
||||
)
|
||||
|
||||
layout = Layout([3])
|
||||
self.add_layout(layout)
|
||||
layout.add_widget(Button("Create Arcade", self._create_arcade))
|
||||
layout.add_widget(Button("Lookup Arcade", self._lookup))
|
||||
|
||||
def _redraw(self):
|
||||
self._layouts = [self._layouts[0]]
|
||||
|
||||
layout = Layout([3])
|
||||
self.add_layout(layout)
|
||||
layout.add_widget(Button("Edit Arcade", self._edit_arcade, disabled=state.selected_arcade.id == 0 or state.selected_arcade.id is None))
|
||||
layout.add_widget(Button("Delete Arcade", self._del_arcade, disabled=state.selected_arcade.id == 0 or state.selected_arcade.id is None))
|
||||
layout.add_widget((Divider()))
|
||||
|
||||
layout2 = Layout([1, 1, 1])
|
||||
self.add_layout(layout2)
|
||||
a = Text("", f"status", readonly=True, disabled=True)
|
||||
a.value = f"Selected Arcade: {state.selected_arcade}"
|
||||
layout2.add_widget(a)
|
||||
layout2.add_widget(Button("Back", self._back), 2)
|
||||
|
||||
self.fix()
|
||||
|
||||
def _create_arcade(self):
|
||||
self.save()
|
||||
raise NextScene("Create Arcade")
|
||||
|
||||
def _lookup(self):
|
||||
self.save()
|
||||
raise NextScene("Lookup Arcade")
|
||||
|
||||
def _edit_arcade(self):
|
||||
self.save()
|
||||
raise NextScene("Edit Arcade")
|
||||
|
||||
def _del_arcade(self):
|
||||
self.save()
|
||||
raise NextScene("Delete Arcade")
|
||||
|
||||
def _back(self):
|
||||
self.save()
|
||||
raise NextScene("Main")
|
||||
|
||||
class CreateArcadeView(Frame):
|
||||
def __init__(self, screen: Screen):
|
||||
super(CreateArcadeView, self).__init__(
|
||||
screen,
|
||||
screen.height * 2 // 3,
|
||||
screen.width * 2 // 3,
|
||||
hover_focus=True,
|
||||
can_scroll=False,
|
||||
title="Create Arcade"
|
||||
)
|
||||
|
||||
layout = Layout([100], fill_frame=True)
|
||||
self.add_layout(layout)
|
||||
layout.add_widget(Text("Name:", "name"))
|
||||
layout.add_widget(Text("Nickname:", "nickname"))
|
||||
layout.add_widget(Text("Timezone:", "timezone"))
|
||||
layout.add_widget(Text("VPN IP:", "ip"))
|
||||
layout.add_widget(CheckBox("", "Add Machine:", "is_add_machine", ))
|
||||
layout.add_widget(RadioButtons([
|
||||
(inflection.titleize(x.name), x.value) for x in AllnetCountryCode
|
||||
], "Country:", "country"))
|
||||
|
||||
layout3 = Layout([100])
|
||||
self.add_layout(layout3)
|
||||
layout3.add_widget(Text("", f"status", readonly=True, disabled=True))
|
||||
|
||||
layout2 = Layout([1, 1, 1, 1])
|
||||
self.add_layout(layout2)
|
||||
layout2.add_widget(Button("OK", self._ok), 0)
|
||||
layout2.add_widget(Button("Cancel", self._cancel), 3)
|
||||
|
||||
self.fix()
|
||||
|
||||
def _ok(self):
|
||||
self.save()
|
||||
state.clear_last_err()
|
||||
self.find_widget('status').value = state.last_err
|
||||
|
||||
loop.run_until_complete(self._create_arcade_async(self.data.get("name"), self.data.get("nickname"), self.data.get("country"), self.data.get('timezone'), self.data.get('ip')))
|
||||
|
||||
raise NextScene("Create Machine" if self.data.get("is_add_machine") else "Arcade Management")
|
||||
|
||||
async def _create_arcade_async(self, name: str, nickname: Optional[str], country: str, timezone: Optional[str], ip: Optional[str]):
|
||||
arcade_id = await data.arcade.create_arcade(name, nickname if nickname != "" else None, country)
|
||||
|
||||
if timezone:
|
||||
data.arcade.set_arcade_timezone(arcade_id, timezone)
|
||||
|
||||
if ip:
|
||||
data.arcade.set_arcade_vpn_ip(arcade_id, ip)
|
||||
|
||||
if arcade_id:
|
||||
state.set_arcade(arcade_id, country, name)
|
||||
|
||||
def _cancel(self):
|
||||
state.clear_last_err()
|
||||
self.find_widget('status').value = state.last_err
|
||||
raise NextScene("Arcade Management")
|
||||
|
||||
class LookupArcadeView(Frame):
|
||||
def __init__(self, screen):
|
||||
super(LookupArcadeView, self).__init__(
|
||||
screen,
|
||||
screen.height * 2 // 3,
|
||||
screen.width * 2 // 3,
|
||||
hover_focus=True,
|
||||
can_scroll=False,
|
||||
title="Lookup Arcade"
|
||||
)
|
||||
|
||||
layout = Layout([1, 1], fill_frame=True)
|
||||
self.add_layout(layout)
|
||||
layout.add_widget(RadioButtons([
|
||||
("Name", "1"),
|
||||
("Serial", "2"),
|
||||
("Place ID", "3"),
|
||||
("Arcade ID", "4"),
|
||||
], "Search By:", "search_type"))
|
||||
layout.add_widget(Text("Search:", "search_str"), 1)
|
||||
|
||||
layout3 = Layout([100])
|
||||
self.add_layout(layout3)
|
||||
layout3.add_widget(Text("", f"status", readonly=True, disabled=True))
|
||||
|
||||
layout2 = Layout([1, 1, 1, 1])
|
||||
self.add_layout(layout2)
|
||||
layout2.add_widget(Button("Search", self._lookup), 0)
|
||||
layout2.add_widget(Button("Cancel", self._cancel), 3)
|
||||
|
||||
self.fix()
|
||||
|
||||
def _lookup(self):
|
||||
self.save()
|
||||
if not self.data.get("search_str"):
|
||||
state.set_last_err("Search cannot be blank")
|
||||
self.find_widget('status').value = state.last_err
|
||||
self.screen.reset()
|
||||
return
|
||||
|
||||
state.clear_last_err()
|
||||
self.find_widget('status').value = state.last_err
|
||||
|
||||
search_type = self.data.get("search_type")
|
||||
if search_type == "1":
|
||||
loop.run_until_complete(self._lookup_arcade_by_name(self.data.get("search_str")))
|
||||
elif search_type == "2":
|
||||
loop.run_until_complete(self._lookup_arcade_by_serial(self.data.get("search_str")))
|
||||
elif search_type == "3":
|
||||
real_id = int(self.data.get("search_str"), 16)
|
||||
loop.run_until_complete(self._lookup_arcade_by_id(real_id))
|
||||
elif search_type == "4":
|
||||
loop.run_until_complete(self._lookup_arcade_by_id(self.data.get("search_str")))
|
||||
else:
|
||||
state.set_last_err("Unknown search type")
|
||||
self.find_widget('status').value = state.last_err
|
||||
self.screen.reset()
|
||||
return
|
||||
|
||||
if len(state.search_results) < 1:
|
||||
state.set_last_err("Search returned no results")
|
||||
self.find_widget('status').value = state.last_err
|
||||
self.screen.reset()
|
||||
return
|
||||
|
||||
state.search_type = "user"
|
||||
raise NextScene("Search Results")
|
||||
|
||||
async def _lookup_arcade_by_id(self, ac_id: str):
|
||||
ac = await data.arcade.get_arcade(ac_id)
|
||||
|
||||
if ac is not None:
|
||||
res = ac._asdict()
|
||||
num_cabs = await data.arcade.get_arcade_machines(ac_id)
|
||||
res['mech_ct'] = len(num_cabs) if num_cabs else 0
|
||||
state.search_results = [res]
|
||||
|
||||
async def _lookup_arcade_by_name(self, name: str):
|
||||
ac = await data.arcade.get_arcade_by_name(name)
|
||||
|
||||
if ac is not None:
|
||||
res = []
|
||||
for ac_res in ac:
|
||||
t = ac_res._asdict()
|
||||
num_cabs = await data.arcade.get_arcade_machines(t['id'])
|
||||
t['mech_ct'] = len(num_cabs) if num_cabs else 0
|
||||
res.append(t)
|
||||
|
||||
state.search_results = res
|
||||
|
||||
async def _lookup_arcade_by_serial(self, serial: str):
|
||||
mech = await data.arcade.get_machine(serial)
|
||||
|
||||
if mech is not None:
|
||||
ac = await data.arcade.get_arcade(mech['arcade'])
|
||||
|
||||
if ac is not None:
|
||||
res = ac._asdict()
|
||||
num_cabs = await data.arcade.get_arcade_machines(mech['arcade'])
|
||||
res['mech_ct'] = len(num_cabs) if num_cabs else 0
|
||||
state.search_results = [res]
|
||||
|
||||
def _cancel(self):
|
||||
state.clear_last_err()
|
||||
self.find_widget('status').value = state.last_err
|
||||
raise NextScene("Arcade Management")
|
||||
|
||||
class ManageMachineView(Frame):
|
||||
def __init__(self, screen: Screen):
|
||||
super(ManageMachineView, self).__init__(
|
||||
screen,
|
||||
screen.height * 2 // 3,
|
||||
screen.width * 2 // 3,
|
||||
hover_focus=True,
|
||||
can_scroll=False,
|
||||
title="Machine Management",
|
||||
on_load=self._redraw
|
||||
)
|
||||
|
||||
layout = Layout([3])
|
||||
self.add_layout(layout)
|
||||
layout.add_widget(Button("Create Machine", self._create))
|
||||
layout.add_widget(Button("Lookup Machine", self._lookup))
|
||||
|
||||
def _redraw(self):
|
||||
self._layouts = [self._layouts[0]]
|
||||
|
||||
layout = Layout([3])
|
||||
self.add_layout(layout)
|
||||
layout.add_widget(Button("Reassign Machine", self._reassign, disabled=state.selected_machine.id == 0 or state.selected_machine.id is None))
|
||||
layout.add_widget(Button("Edit Machine", self._edit, disabled=state.selected_machine.id == 0 or state.selected_machine.id is None))
|
||||
layout.add_widget(Button("Delete Machine", self._del, disabled=state.selected_machine.id == 0 or state.selected_machine.id is None))
|
||||
layout.add_widget((Divider()))
|
||||
|
||||
layout2 = Layout([1, 1, 1])
|
||||
self.add_layout(layout2)
|
||||
a = Text("", f"status", readonly=True, disabled=True)
|
||||
a.value = f"Selected Machine: {state.selected_arcade}"
|
||||
layout2.add_widget(a)
|
||||
layout2.add_widget(Button("Back", self._back), 2)
|
||||
|
||||
self.fix()
|
||||
|
||||
def _create(self):
|
||||
self.save()
|
||||
raise NextScene("Create Machine")
|
||||
|
||||
def _lookup(self):
|
||||
self.save()
|
||||
raise NextScene("Lookup Machine")
|
||||
|
||||
def _reassign(self):
|
||||
self.save()
|
||||
raise NextScene("Reassign Machine")
|
||||
|
||||
def _edit(self):
|
||||
self.save()
|
||||
raise NextScene("Edit Machine")
|
||||
|
||||
def _del(self):
|
||||
self.save()
|
||||
raise NextScene("Delete Machine")
|
||||
|
||||
def _back(self):
|
||||
self.save()
|
||||
raise NextScene("Main")
|
||||
|
||||
class CreateMachineView(Frame):
|
||||
def __init__(self, screen: Screen):
|
||||
super(CreateMachineView, self).__init__(
|
||||
screen,
|
||||
screen.height - 10,
|
||||
screen.width * 2 // 3,
|
||||
hover_focus=True,
|
||||
can_scroll=False,
|
||||
title="Create Machine"
|
||||
)
|
||||
|
||||
layout = Layout([100], fill_frame=True)
|
||||
self.add_layout(layout)
|
||||
layout.add_widget(Text("Arcade:", "arcade_id"))
|
||||
layout.add_widget(Text("Serial:", "serial"))
|
||||
layout.add_widget(Text("Game:", "game_id"))
|
||||
layout.add_widget(CheckBox("", "Real Cabinet:", "is_cab", ))
|
||||
layout.add_widget(RadioButtons([("Not Set", None)] + [
|
||||
(inflection.titleize(x.name), x.value) for x in AllnetCountryCode
|
||||
], "Country Override:", "country"))
|
||||
|
||||
layout3 = Layout([100])
|
||||
self.add_layout(layout3)
|
||||
layout3.add_widget(Text("", f"status", readonly=True, disabled=True))
|
||||
|
||||
layout2 = Layout([1, 1, 1, 1])
|
||||
self.add_layout(layout2)
|
||||
layout2.add_widget(Button("OK", self._ok), 0)
|
||||
layout2.add_widget(Button("Cancel", self._cancel), 3)
|
||||
|
||||
self.fix()
|
||||
|
||||
def _redraw(self):
|
||||
self.find_widget("arcade_id").value = state.selected_arcade.id
|
||||
|
||||
def _ok(self):
|
||||
self.save()
|
||||
state.clear_last_err()
|
||||
self.find_widget('status').value = state.last_err
|
||||
|
||||
loop.run_until_complete(self._create_arcade_async(self.data.get("name"), self.data.get("nickname"), self.data.get("country"), self.data.get('timezone'), self.data.get('ip')))
|
||||
|
||||
raise NextScene("Arcade Management")
|
||||
|
||||
async def _create_arcade_async(self, arcade_id: int, serial: str, game: Optional[str], is_cab: bool, country: Optional[str]):
|
||||
machine_id = await data.arcade.create_machine(arcade_id, serial, None, game, is_cab)
|
||||
|
||||
if country:
|
||||
data.arcade.set_machine_country(machine_id, country)
|
||||
|
||||
if machine_id:
|
||||
state.set_machine(machine_id, serial)
|
||||
|
||||
def _cancel(self):
|
||||
state.clear_last_err()
|
||||
self.find_widget('status').value = state.last_err
|
||||
raise NextScene("Machine Management")
|
||||
|
||||
class LookupMachineView(Frame):
|
||||
def __init__(self, screen):
|
||||
super(LookupMachineView, self).__init__(
|
||||
screen,
|
||||
screen.height * 2 // 3,
|
||||
screen.width * 2 // 3,
|
||||
hover_focus=True,
|
||||
can_scroll=False,
|
||||
title="Lookup Machine"
|
||||
)
|
||||
|
||||
layout = Layout([1, 1], fill_frame=True)
|
||||
self.add_layout(layout)
|
||||
layout.add_widget(RadioButtons([
|
||||
("Name", "1"),
|
||||
("Serial", "2"),
|
||||
("Arcade ID", "3"),
|
||||
("Machine ID", "4"),
|
||||
], "Search By:", "search_type"))
|
||||
layout.add_widget(Text("Search:", "search_str"), 1)
|
||||
|
||||
layout3 = Layout([100])
|
||||
self.add_layout(layout3)
|
||||
layout3.add_widget(Text("", f"status", readonly=True, disabled=True))
|
||||
|
||||
layout2 = Layout([1, 1, 1, 1])
|
||||
self.add_layout(layout2)
|
||||
layout2.add_widget(Button("Search", self._lookup), 0)
|
||||
layout2.add_widget(Button("Cancel", self._cancel), 3)
|
||||
|
||||
self.fix()
|
||||
|
||||
def _lookup(self):
|
||||
self.save()
|
||||
if not self.data.get("search_str"):
|
||||
state.set_last_err("Search cannot be blank")
|
||||
self.find_widget('status').value = state.last_err
|
||||
self.screen.reset()
|
||||
return
|
||||
|
||||
state.clear_last_err()
|
||||
self.find_widget('status').value = state.last_err
|
||||
|
||||
search_type = self.data.get("search_type")
|
||||
if search_type == "1":
|
||||
loop.run_until_complete(self._lookup_arcade_by_name(self.data.get("search_str")))
|
||||
elif search_type == "2":
|
||||
loop.run_until_complete(self._lookup_arcade_by_serial(self.data.get("search_str")))
|
||||
elif search_type == "3":
|
||||
real_id = int(self.data.get("search_str"), 16)
|
||||
loop.run_until_complete(self._lookup_arcade_by_id(real_id))
|
||||
elif search_type == "4":
|
||||
loop.run_until_complete(self._lookup_arcade_by_id(self.data.get("search_str")))
|
||||
else:
|
||||
state.set_last_err("Unknown search type")
|
||||
self.find_widget('status').value = state.last_err
|
||||
self.screen.reset()
|
||||
return
|
||||
|
||||
if len(state.search_results) < 1:
|
||||
state.set_last_err("Search returned no results")
|
||||
self.find_widget('status').value = state.last_err
|
||||
self.screen.reset()
|
||||
return
|
||||
|
||||
state.search_type = "user"
|
||||
raise NextScene("Search Results")
|
||||
|
||||
async def _lookup_arcade_by_id(self, ac_id: str):
|
||||
ac = await data.arcade.get_arcade(ac_id)
|
||||
|
||||
if ac is not None:
|
||||
res = ac._asdict()
|
||||
num_cabs = await data.arcade.get_arcade_machines(ac_id)
|
||||
res['mech_ct'] = len(num_cabs) if num_cabs else 0
|
||||
state.search_results = [res]
|
||||
|
||||
async def _lookup_arcade_by_name(self, name: str):
|
||||
ac = await data.arcade.get_arcade_by_name(name)
|
||||
|
||||
if ac is not None:
|
||||
res = []
|
||||
for ac_res in ac:
|
||||
t = ac_res._asdict()
|
||||
num_cabs = await data.arcade.get_arcade_machines(t['id'])
|
||||
t['mech_ct'] = len(num_cabs) if num_cabs else 0
|
||||
res.append(t)
|
||||
|
||||
state.search_results = res
|
||||
|
||||
async def _lookup_arcade_by_serial(self, serial: str):
|
||||
mech = await data.arcade.get_machine(serial)
|
||||
|
||||
if mech is not None:
|
||||
ac = await data.arcade.get_arcade(mech['arcade'])
|
||||
|
||||
if ac is not None:
|
||||
res = ac._asdict()
|
||||
num_cabs = await data.arcade.get_arcade_machines(mech['arcade'])
|
||||
res['mech_ct'] = len(num_cabs) if num_cabs else 0
|
||||
state.search_results = [res]
|
||||
|
||||
def _cancel(self):
|
||||
state.clear_last_err()
|
||||
self.find_widget('status').value = state.last_err
|
||||
raise NextScene("Machine Management")
|
||||
|
||||
def demo(screen:Screen, scene: Scene):
|
||||
scenes = [
|
||||
Scene([MainView(screen)], -1, name="Main"),
|
||||
Scene([ManageUser(screen)], -1, name="User Management"),
|
||||
Scene([ManageUserView(screen)], -1, name="User Management"),
|
||||
Scene([CreateUserView(screen)], -1, name="Create User"),
|
||||
Scene([LookupUserView(screen)], -1, name="Lookup User"),
|
||||
Scene([SearchResultsView(screen)], -1, name="Search Results"),
|
||||
Scene([ManageCard(screen)], -1, name="Card Management"),
|
||||
Scene([ManageCardView(screen)], -1, name="Card Management"),
|
||||
Scene([EditUserView(screen)], -1, name="Edit User"),
|
||||
Scene([ManageArcadeView(screen)], -1, name="Arcade Management"),
|
||||
Scene([CreateArcadeView(screen)], -1, name="Create Arcade"),
|
||||
Scene([LookupArcadeView(screen)], -1, name="Lookup Arcade"),
|
||||
Scene([ManageMachineView(screen)], -1, name="Machine Management"),
|
||||
Scene([LookupMachineView(screen)], -1, name="Lookup Machine"),
|
||||
Scene([CreateMachineView(screen)], -1, name="Create Machine"),
|
||||
]
|
||||
|
||||
screen.play(scenes, stop_on_resize=False, start_scene=scene, allow_int=True)
|
||||
|
Reference in New Issue
Block a user