55 Commits

Author SHA1 Message Date
e11db14292 Merge pull request '[mai2] Prism Plus support' (#232) from SoulGateKey/artemis:prism_plus_support into develop
Reviewed-on: #232
2025-10-07 17:59:48 +00:00
77152bf25c change down_revision when final merge 2025-10-07 17:59:02 +00:00
f346d8572d chuni: fix typo in upgrade script 2025-10-07 13:26:54 -04:00
ce621065a4 Merge pull request 'CHUNITHM VERSE support' (#224) from feature/chuni_verse_support into develop
Reviewed-on: #224
2025-09-27 20:20:21 +00:00
2d84865155 add ota update channels 2025-09-27 16:17:44 -04:00
10d38e14ae config: fix typo preventing ssl_cert from working correctly 2025-09-26 13:59:12 -04:00
8194520cca Merge pull request 'Remove duplicate get_opts in Ongeki static' (#228) from Kayori/artemis:smallcleanup into develop
Reviewed-on: #228
2025-09-24 00:24:21 +00:00
2612fc984c tui: more work 2025-09-21 00:33:35 -04:00
dd546dcce2 idz: pretty up no key message, fix double handshake 2025-09-20 16:07:30 -04:00
21415de775 add lut 2025-09-19 23:13:57 +08:00
d16ebe27d9 Merge pull request 'develop' (#16) from develop into prism_plus_support
Reviewed-on: SoulGateKey/artemis#16
2025-09-18 13:05:46 +00:00
5ec6cc0398 tui: arcade management view 2025-09-17 18:08:34 -04:00
c92ede9e55 tui: add edit user view 2025-09-17 12:38:14 -04:00
92422684ef sao: fix error on unknown response type 2025-09-16 22:50:06 -04:00
3df0f3fb06 idac: fix incorrect game_cfg variable name 2025-09-16 21:34:17 -04:00
0c800759bb diva: fix binary requests not being awaited 2025-09-16 21:31:27 -04:00
3ad56306bf remove unused funcs from TitleServlet 2025-09-16 20:59:35 -04:00
d5c68a624f billing: bomb out early if we have unsent logs to avoid duplicating work that's never used 2025-09-16 18:11:52 -04:00
fa18b4c6a2 Merge pull request 'develop' (#15) from develop into prism_plus_support
Reviewed-on: SoulGateKey/artemis#15
2025-09-16 17:54:52 +00:00
7e254a0281 Merge pull request '[Mai2] enhance music score path handling and improve error logging' (#226) from SoulGateKey/artemis:mai2_enhance_musicscoreapi into develop
Reviewed-on: #226
2025-09-16 17:37:52 +00:00
abe480d007 remove duplicate method 2025-08-23 20:35:58 +02:00
cccb5ce1a7 Merge branch 'develop' into mai2_enhance_musicscoreapi 2025-08-20 18:28:49 +00:00
064f2b6b54 enhance music score path handling and improve error logging 2025-08-21 02:27:29 +08:00
b62c89b749 merge upstream 2025-08-20 17:45:22 +00:00
46d79d156b Merge branch 'mai2_rival_support' into prism_plus_support 2025-08-01 00:55:12 +08:00
fb4e10c2ae merge upstream 2025-07-29 12:18:03 +00:00
15e8eb535b Merge remote-tracking branch 'origin/prism_plus_support' into prism_plus_support 2025-07-28 20:06:24 +08:00
392fdb3783 Merge branch 'mai2_rival_support' into prism_plus_support 2025-07-28 01:04:00 +08:00
91545bb974 Merge branch 'mai2_rival_support' into prism_plus_support 2025-07-26 18:43:37 +08:00
e52362d87f Merge pull request 'develop' (#14) from develop into prism_plus_support
Reviewed-on: SoulGateKey/artemis#14
2025-07-25 18:16:21 +00:00
e3ec58b238 Merge branch 'mai2_MusicScoreApi_support' into prism_plus_support 2025-07-26 01:38:45 +08:00
0a4dc8dbb0 add Gate 7,8,9,10 support 2025-07-22 19:55:33 +08:00
180c027575 Sync Develop branch's update 2025-07-22 03:37:39 +08:00
a86b8eeddb merge upstream 2025-07-13 21:35:02 +00:00
d72603d101 merge upstream 2025-07-01 19:44:59 +00:00
611806828a add Gate 5,6 judgement 2025-05-15 08:19:42 +08:00
8b050e89eb fix conflict mark 2025-05-15 03:53:42 +08:00
394ec74fb7 add key unlock condition define 2025-05-15 03:14:24 +08:00
a2a333b13f Merge remote-tracking branch 'origin/prism_plus_support' into prism_plus_support
# Conflicts:
#	titles/mai2/index.py
2025-05-15 02:45:56 +08:00
933d8bea21 add import 2025-05-15 02:44:37 +08:00
f70af35343 Merge branch 'refs/heads/develop' into prism_plus_support
# Conflicts:
#	core/data/alembic/versions/16f34bf7b968_mai2_kaleidx_scope_support.py
#	core/data/alembic/versions/5cf98cfe52ad_mai2_prism_support.py
#	core/data/alembic/versions/5d7b38996e67_mai2_prism_support.py
#	core/data/alembic/versions/bdf710616ba4_mai2_add_prism_playlog_support.py
#	titles/mai2/index.py
#	titles/mai2/prism.py
#	titles/mai2/read.py
#	titles/mai2/schema/static.py
2025-05-15 02:41:55 +08:00
1e7f367d0f Merge pull request 'develop' (#9) from develop into prism_plus_support
Reviewed-on: SoulGateKey/artemis#9
2025-04-08 04:37:17 +00:00
134af15ed7 update readme.md and game_specific_info.md 2025-04-07 09:15:29 +08:00
a4bcca9171 add clientplaytimeapi support 2025-04-07 09:15:29 +08:00
d598c8fba0 add prism+ playlog support 2025-04-07 09:15:29 +08:00
3b7a577ea2 add prism+ consts and cm support 2025-04-07 09:15:29 +08:00
dd10508e68 unused database deleted 2025-04-04 09:12:08 +08:00
c0df7cd084 database support for prism
kaleidxScope Key Condition store
2025-04-04 09:10:41 +08:00
756c7ce951 update readme.md and game_specific_info.md 2025-04-02 13:56:56 +08:00
4ceac7db35 add clientplaytimeapi support 2025-04-02 13:49:46 +08:00
f8888c2392 add prism+ playlog support 2025-04-02 12:42:40 +08:00
9bc18f179d add prism+ consts and cm support 2025-04-02 12:37:43 +08:00
eb66c9159f Merge pull request 'develop' (#12) from Hay1tsme/artemis:develop into develop
Reviewed-on: Kayori/artemis#12
2024-08-21 09:40:35 +00:00
5c45091cec Merge pull request 'develop' (#11) from Hay1tsme/artemis:develop into develop
Reviewed-on: ThatzOkay/artemis#11
2024-07-19 12:22:22 +00:00
00d3b6e69a Merge pull request 'develop' (#10) from Hay1tsme/artemis:develop into develop
Reviewed-on: ThatzOkay/artemis#10
2024-07-09 22:48:20 +00:00
26 changed files with 1000 additions and 162 deletions

View File

@ -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(
@ -445,16 +445,14 @@ class AllnetServlet:
iv = bytes([random.randint(2, 255) for _ in range(16)])
return PlainTextResponse(content=self.enc_lite(litekey, iv, resp))
return PlainTextResponse(resp)
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('.', '')}-app.ini"
):
resp.uri = f"http://{self.config.server.hostname}:{self.config.server.port}/dl/ini/{req.game_id}-{req.ver.replace('.', '')}-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,8 +803,9 @@ 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:
playlimit += 1024
@ -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)

View File

@ -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

View File

@ -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",

View File

@ -10,6 +10,7 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '5cf98cfe52ad'
down_revision = '263884e774cc'
branch_labels = None

View 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 ###

View File

@ -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 ###

View File

@ -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
@ -433,7 +446,7 @@ class ArcadeData(BaseData):
self.logger.error(f"Failed to add billing charge for machine {machine_id}!")
return None
return result.lastrowid
async def billing_get_last_charge(self, machine_id: int, game_id: str) -> Optional[Row]:
result = await self.execute(billing_charge.select(
and_(billing_charge.c.machine == machine_id, billing_charge.c.game_id == game_id)
@ -511,7 +524,7 @@ class ArcadeData(BaseData):
if result is None:
self.logger.error(f"Failed to add playcount for machine {machine_id} running {game_id}")
async def billing_get_playcount_3mo(self, machine_id: int, game_id: str) -> Optional[List[Row]]:
result = await self.execute(billing_playct.select(and_(
billing_playct.c.machine == machine_id,
@ -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:

View File

@ -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

View File

@ -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:

View File

@ -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>

View File

@ -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)

View File

@ -205,32 +205,32 @@ 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 |
| SBZF | 3 | maimai GreeN PLUS |
| SDBM | 4 | maimai ORANGE |
| SDBM | 5 | maimai ORANGE PLUS |
| SDCQ | 6 | maimai PiNK |
| SDCQ | 7 | maimai PiNK PLUS |
| SDDK | 8 | maimai MURASAKi |
| SDDK | 9 | maimai MURASAKi PLUS |
| SDDZ | 10 | maimai MiLK |
| SDDZ | 11 | maimai MiLK PLUS |
| SDEY | 12 | maimai FiNALE |
| SDEZ | 13 | maimai DX |
| SDEZ | 14 | maimai DX PLUS |
| SDEZ | 15 | maimai DX Splash |
| SDEZ | 16 | maimai DX Splash PLUS |
| SDEZ | 17 | maimai DX UNiVERSE |
| SDEZ | 18 | maimai DX UNiVERSE PLUS |
| SDEZ | 19 | maimai DX FESTiVAL |
| SDEZ | 20 | maimai DX FESTiVAL PLUS |
| SDEZ | 21 | maimai DX BUDDiES |
| SDEZ | 22 | maimai DX BUDDiES PLUS |
| SDEZ | 23 | maimai DX PRiSM |
|----------|------------|-------------------------|
| SBXL | 0 | maimai |
| SBXL | 1 | maimai PLUS |
| SBZF | 2 | maimai GreeN |
| SBZF | 3 | maimai GreeN PLUS |
| SDBM | 4 | maimai ORANGE |
| SDBM | 5 | maimai ORANGE PLUS |
| SDCQ | 6 | maimai PiNK |
| SDCQ | 7 | maimai PiNK PLUS |
| SDDK | 8 | maimai MURASAKi |
| SDDK | 9 | maimai MURASAKi PLUS |
| SDDZ | 10 | maimai MiLK |
| SDDZ | 11 | maimai MiLK PLUS |
| SDEY | 12 | maimai FiNALE |
| SDEZ | 13 | maimai DX |
| SDEZ | 14 | maimai DX PLUS |
| SDEZ | 15 | maimai DX Splash |
| SDEZ | 16 | maimai DX Splash PLUS |
| SDEZ | 17 | maimai DX UNiVERSE |
| SDEZ | 18 | maimai DX UNiVERSE PLUS |
| SDEZ | 19 | maimai DX FESTiVAL |
| SDEZ | 20 | maimai DX FESTiVAL PLUS |
| 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

View File

@ -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

View File

@ -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):

View File

@ -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}")

View File

@ -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,
}

View File

@ -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

View File

@ -53,28 +53,31 @@ 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:
base = 0
if not sent_handshake:
base = 0
for i in range(len(self.static_key) - 1):
shift = 8 * i
byte = self.static_key[i]
for i in range(len(self.static_key) - 1):
shift = 8 * i
byte = self.static_key[i]
base |= byte << shift
base |= byte << shift
rsa_key = random.choice(self.rsa_keys)
key_enc: int = pow(base, rsa_key.e, rsa_key.N)
result = (
key_enc.to_bytes(0x40, "little")
+ struct.pack("<I", 0x01020304)
+ rsa_key.hashN.to_bytes(4, "little")
)
rsa_key = random.choice(self.rsa_keys)
key_enc: int = pow(base, rsa_key.e, rsa_key.N)
result = (
key_enc.to_bytes(0x40, "little")
+ struct.pack("<I", 0x01020304)
+ rsa_key.hashN.to_bytes(4, "little")
)
self.logger.debug(f"Send handshake {result.hex()}")
self.logger.debug(f"Send handshake {result.hex()}")
writer.write(result)
await writer.drain()
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)

View File

@ -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

View File

@ -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")
@ -195,7 +197,7 @@ class Mai2Servlet(BaseServlet):
if proto == "" or proto == "https://":
t_port = f":{title_port_ssl_int}" if title_port_ssl_int != 443 else ""
else:
else:
t_port = f":{title_port_int}" if title_port_int != 80 else ""
return (
@ -343,10 +345,12 @@ class Mai2Servlet(BaseServlet):
internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL_PLUS
elif version >= 140 and version < 145: # BUDDiES
internal_ver = Mai2Constants.VER_MAIMAI_DX_BUDDIES
elif version >= 145 and version <150: # BUDDiES PLUS
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
@ -367,10 +371,12 @@ class Mai2Servlet(BaseServlet):
internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL_PLUS
elif version >= 140 and version < 145: # BUDDiES
internal_ver = Mai2Constants.VER_MAIMAI_DX_BUDDIES
elif version >= 145 and version <150: # BUDDiES PLUS
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":

View File

@ -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
View 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
}

View File

@ -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",
)

View File

@ -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))

View File

@ -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()}")

599
tui.py
View File

@ -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,
@ -415,6 +418,19 @@ class SearchResultsView(Frame):
name = usr['username'][:5] + "..."
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"))
@ -423,12 +439,25 @@ class SearchResultsView(Frame):
def _select_current(self):
self.save()
a = self.data.get('selopt')
state.set_user(a.id, a.name)
raise NextScene("User Management")
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()
raise NextScene("User Management")
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):
@ -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)