From e0e63a9a13070d271e0f01dfdf5d033ac5a3d978 Mon Sep 17 00:00:00 2001 From: = <=> Date: Sun, 30 Jun 2024 00:23:10 +0200 Subject: [PATCH 1/7] configurable festa options --- changelog.md | 4 ++++ docs/game_specific_info.md | 12 ++++++++---- example_config/diva.yaml | 4 ++++ titles/diva/base.py | 11 ++++++++--- titles/diva/config.py | 23 +++++++++++++++++++++++ 5 files changed, 47 insertions(+), 7 deletions(-) diff --git a/changelog.md b/changelog.md index 8148a9d..08b5b6d 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,10 @@ # Changelog Documenting updates to ARTEMiS, to be updated every time the master branch is pushed to. +## 20240630 +### DIVA ++ Added configurable festa options + ## 20240620 ### CHUNITHM + CHUNITHM LUMINOUS support diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index 53c076c..158930e 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -255,10 +255,14 @@ the Shop, Modules and Customizations. Config file is located in `config/diva.yaml`. -| Option | Info | -| -------------------- | ----------------------------------------------------------------------------------------------- | -| `unlock_all_modules` | Unlocks all modules (costumes) by default, if set to `False` all modules need to be purchased | -| `unlock_all_items` | Unlocks all items (customizations) by default, if set to `False` all items need to be purchased | +| Option | Info | +| -------------------- | ------------------------------------------------------------------------------------------------ | +| `festaEnable` | Enable or disable the ingame festa | +| `festaAddVP` | Set the extra VP you get when clearing a song, if festa is not enabled no extra VP will be given | +| `festaMultiplyVP` | Multiplier for festa add VP | +| `festaEndTime` | Set the date time for when festa will end and not show up in game anymore | +| `unlock_all_modules` | Unlocks all modules (costumes) by default, if set to `False` all modules need to be purchased | +| `unlock_all_items` | Unlocks all items (customizations) by default, if set to `False` all items need to be purchased | ### Custom PV Lists (databanks) diff --git a/example_config/diva.yaml b/example_config/diva.yaml index ad1842a..465ab93 100644 --- a/example_config/diva.yaml +++ b/example_config/diva.yaml @@ -1,6 +1,10 @@ server: enable: True loglevel: "info" + festaEnable: True + festaAddVP: "20,5" + festaMultiplyVP: "1,2" + festaEndTime: "2029-01-01 00:00:00.0" mods: unlock_all_modules: True diff --git a/titles/diva/base.py b/titles/diva/base.py index 3b96848..e07c81d 100644 --- a/titles/diva/base.py +++ b/titles/diva/base.py @@ -264,6 +264,11 @@ class DivaBase: return response async def handle_festa_info_request(self, data: Dict) -> Dict: + if self.game_config.server.festaEnable: + festa_end_time = self.game_config.server.festaEndTime + else: + festa_end_time = (datetime.datetime.now() + datetime.timedelta(days=365*2)).strftime("%Y-%m-%d %H:%M:%S") + ".0" + encoded = "&" params = { "fi_id": "1,2", @@ -273,10 +278,10 @@ class DivaBase: "fi_difficulty": "-1,-1", "fi_pv_id_lst": "ALL,ALL", "fi_attr": "7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", - "fi_add_vp": "20,5", - "fi_mul_vp": "1,2", + "fi_add_vp": f"{self.game_config.server.festaAddVP}", + "fi_mul_vp": f"{self.game_config.server.festaMultiplyVP}", "fi_st": "2019-01-01 00:00:00.0,2019-01-01 00:00:00.0", - "fi_et": "2029-01-01 00:00:00.0,2029-01-01 00:00:00.0", + "fi_et": f"{festa_end_time},{festa_end_time}", "fi_lut": "{self.time_lut}", } diff --git a/titles/diva/config.py b/titles/diva/config.py index efa327e..269041a 100644 --- a/titles/diva/config.py +++ b/titles/diva/config.py @@ -19,6 +19,29 @@ class DivaServerConfig: ) ) + @property + def festaEnable(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "diva", "server", "festaEnable", default=True + ) + + @property + def festaAddVP(self) -> str: + return CoreConfig.get_config_field( + self.__config, "diva", "server", "festaAddVP", default="20,5" + ) + + @property + def festaMultiplyVP(self) -> str: + return CoreConfig.get_config_field( + self.__config, "diva", "server", "festaMultiplyVP", default="1,2" + ) + + @property + def festaEndTime(self) -> str: + return CoreConfig.get_config_field( + self.__config, "diva", "server", "festaEndTime", default="2029-01-01 00:00:00.0" + ) class DivaModsConfig: def __init__(self, parent_config: "DivaConfig") -> None: From 97e1d5da058953491cc71cd54359e4cfdbbb2bda Mon Sep 17 00:00:00 2001 From: = <=> Date: Sun, 30 Jun 2024 12:54:44 +0200 Subject: [PATCH 2/7] fix disable. Fix casing in options --- docs/game_specific_info.md | 8 ++++---- example_config/diva.yaml | 8 ++++---- titles/diva/base.py | 10 +++++----- titles/diva/config.py | 16 ++++++++-------- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index ecd92e2..33d5710 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -271,10 +271,10 @@ Config file is located in `config/diva.yaml`. | Option | Info | | -------------------- | ------------------------------------------------------------------------------------------------ | -| `festaEnable` | Enable or disable the ingame festa | -| `festaAddVP` | Set the extra VP you get when clearing a song, if festa is not enabled no extra VP will be given | -| `festaMultiplyVP` | Multiplier for festa add VP | -| `festaEndTime` | Set the date time for when festa will end and not show up in game anymore | +| `festa_enable` | Enable or disable the ingame festa | +| `festa_add_VP` | Set the extra VP you get when clearing a song, if festa is not enabled no extra VP will be given | +| `festa_multiply_VP` | Multiplier for festa add VP | +| `festa_end_time` | Set the date time for when festa will end and not show up in game anymore | | `unlock_all_modules` | Unlocks all modules (costumes) by default, if set to `False` all modules need to be purchased | | `unlock_all_items` | Unlocks all items (customizations) by default, if set to `False` all items need to be purchased | diff --git a/example_config/diva.yaml b/example_config/diva.yaml index 465ab93..7b8bdcb 100644 --- a/example_config/diva.yaml +++ b/example_config/diva.yaml @@ -1,10 +1,10 @@ server: enable: True loglevel: "info" - festaEnable: True - festaAddVP: "20,5" - festaMultiplyVP: "1,2" - festaEndTime: "2029-01-01 00:00:00.0" + festa_enable: True + festa_add_VP: "20,5" + festa_multiply_VP: "1,2" + festa_end_time: "2029-01-01 00:00:00.0" mods: unlock_all_modules: True diff --git a/titles/diva/base.py b/titles/diva/base.py index e07c81d..be7a241 100644 --- a/titles/diva/base.py +++ b/titles/diva/base.py @@ -264,10 +264,10 @@ class DivaBase: return response async def handle_festa_info_request(self, data: Dict) -> Dict: - if self.game_config.server.festaEnable: - festa_end_time = self.game_config.server.festaEndTime + if self.game_config.server.festa_enable: + festa_end_time = self.game_config.server.festa_end_time else: - festa_end_time = (datetime.datetime.now() + datetime.timedelta(days=365*2)).strftime("%Y-%m-%d %H:%M:%S") + ".0" + festa_end_time = (datetime.datetime.now() - datetime.timedelta(days=365)).strftime("%Y-%m-%d %H:%M:%S") + ".0" encoded = "&" params = { @@ -278,8 +278,8 @@ class DivaBase: "fi_difficulty": "-1,-1", "fi_pv_id_lst": "ALL,ALL", "fi_attr": "7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", - "fi_add_vp": f"{self.game_config.server.festaAddVP}", - "fi_mul_vp": f"{self.game_config.server.festaMultiplyVP}", + "fi_add_vp": f"{self.game_config.server.festa_add_VP}", + "fi_mul_vp": f"{self.game_config.server.festa_multiply_VP}", "fi_st": "2019-01-01 00:00:00.0,2019-01-01 00:00:00.0", "fi_et": f"{festa_end_time},{festa_end_time}", "fi_lut": "{self.time_lut}", diff --git a/titles/diva/config.py b/titles/diva/config.py index 269041a..48b5c0f 100644 --- a/titles/diva/config.py +++ b/titles/diva/config.py @@ -20,27 +20,27 @@ class DivaServerConfig: ) @property - def festaEnable(self) -> bool: + def festa_enable(self) -> bool: return CoreConfig.get_config_field( - self.__config, "diva", "server", "festaEnable", default=True + self.__config, "diva", "server", "festa_enable", default=True ) @property - def festaAddVP(self) -> str: + def festa_add_VP(self) -> str: return CoreConfig.get_config_field( - self.__config, "diva", "server", "festaAddVP", default="20,5" + self.__config, "diva", "server", "festa_add_VP", default="20,5" ) @property - def festaMultiplyVP(self) -> str: + def festa_multiply_VP(self) -> str: return CoreConfig.get_config_field( - self.__config, "diva", "server", "festaMultiplyVP", default="1,2" + self.__config, "diva", "server", "festa_multiply_VP", default="1,2" ) @property - def festaEndTime(self) -> str: + def festa_end_time(self) -> str: return CoreConfig.get_config_field( - self.__config, "diva", "server", "festaEndTime", default="2029-01-01 00:00:00.0" + self.__config, "diva", "server", "festa_end_time", default="2029-01-01 00:00:00.0" ) class DivaModsConfig: From 9b5283d389ccf1eea5f7f1b5fcea785f779fa32c Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Mon, 1 Jul 2024 18:26:39 -0400 Subject: [PATCH 3/7] mai2: add event editing for sysops --- titles/chuni/frontend.py | 4 +- titles/mai2/frontend.py | 127 +++++++++++++- titles/mai2/schema/static.py | 16 ++ .../templates/events/mai2_event_edit.jinja | 16 ++ .../mai2/templates/events/mai2_events.jinja | 156 ++++++++++++++++++ titles/mai2/templates/mai2_header.jinja | 7 +- 6 files changed, 315 insertions(+), 11 deletions(-) create mode 100644 titles/mai2/templates/events/mai2_event_edit.jinja create mode 100644 titles/mai2/templates/events/mai2_events.jinja diff --git a/titles/chuni/frontend.py b/titles/chuni/frontend.py index 0dbefac..510ae08 100644 --- a/titles/chuni/frontend.py +++ b/titles/chuni/frontend.py @@ -234,11 +234,11 @@ class ChuniFrontend(FE_Base): if usr_sesh.user_id > 0: form_data = await request.form() chunithm_version = form_data.get("version") - self.logger.info(f"version change to: {chunithm_version}") + self.logger.debug(f"version change to: {chunithm_version}") if(chunithm_version.isdigit()): usr_sesh.chunithm_version=int(chunithm_version) encoded_sesh = self.encode_session(usr_sesh) - self.logger.info(f"Created session with JWT {encoded_sesh}") + self.logger.debug(f"Created session with JWT {encoded_sesh}") resp = RedirectResponse("/game/chuni/", 303) resp.set_cookie("ARTEMIS_SESH", encoded_sesh) return resp diff --git a/titles/mai2/frontend.py b/titles/mai2/frontend.py index 635c2fa..976e2c4 100644 --- a/titles/mai2/frontend.py +++ b/titles/mai2/frontend.py @@ -5,8 +5,9 @@ from starlette.responses import Response, RedirectResponse from os import path import yaml import jinja2 +from datetime import datetime -from core.frontend import FE_Base, UserSession +from core.frontend import FE_Base, UserSession, PermissionOffset from core.config import CoreConfig from .database import Mai2Data from .config import Mai2Config @@ -32,6 +33,12 @@ class Mai2Frontend(FE_Base): Route("/", self.render_GET_playlog, methods=['GET']), Route("/{index}", self.render_GET_playlog, methods=['GET']), ]), + Mount("/events", routes=[ + Route("/", self.render_events, methods=['GET']), + Route("/{event_id:int}", self.render_event_edit, methods=['GET']), + Route("/update", self.update_event, methods=['POST']), + Route("/version.change", self.version_change, methods=['POST']), + ]), Route("/update.name", self.update_name, methods=['POST']), Route("/version.change", self.version_change, methods=['POST']), ] @@ -43,13 +50,15 @@ class Mai2Frontend(FE_Base): usr_sesh = self.validate_session(request) if not usr_sesh: usr_sesh = UserSession() + + incoming_ver = usr_sesh.maimai_version if usr_sesh.user_id > 0: versions = await self.data.profile.get_all_profile_versions(usr_sesh.user_id) profile = [] if versions: # maimai_version is -1 means it is not initialized yet, select a default version from existing. - if usr_sesh.maimai_version < 0: + if incoming_ver < 0: usr_sesh.maimai_version = versions[0]['version'] profile = await self.data.profile.get_profile_detail(usr_sesh.user_id, usr_sesh.maimai_version) versions = [x['version'] for x in versions] @@ -65,7 +74,7 @@ class Mai2Frontend(FE_Base): cur_version=usr_sesh.maimai_version ), media_type="text/html; charset=utf-8") - if usr_sesh.maimai_version >= 0: + if incoming_ver < 0: encoded_sesh = self.encode_session(usr_sesh) resp.delete_cookie("ARTEMIS_SESH") resp.set_cookie("ARTEMIS_SESH", encoded_sesh) @@ -80,12 +89,10 @@ class Mai2Frontend(FE_Base): ) usr_sesh = self.validate_session(request) if not usr_sesh: - print("wtf") usr_sesh = UserSession() if usr_sesh.user_id > 0: if usr_sesh.maimai_version < 0: - print(usr_sesh.maimai_version) return RedirectResponse("/game/mai2/", 303) path_index = request.path_params.get('index') if not path_index or int(path_index) < 1: @@ -175,6 +182,11 @@ class Mai2Frontend(FE_Base): if not usr_sesh: usr_sesh = UserSession() + if "/events/" in request.url.path: + resp = RedirectResponse("/game/mai2/events/", 303) + else: + resp = RedirectResponse("/game/mai2/", 303) + if usr_sesh.user_id > 0: form_data = await request.form() maimai_version = form_data.get("version") @@ -182,9 +194,108 @@ class Mai2Frontend(FE_Base): if(maimai_version.isdigit()): usr_sesh.maimai_version=int(maimai_version) encoded_sesh = self.encode_session(usr_sesh) - self.logger.info(f"Created session with JWT {encoded_sesh}") - resp = RedirectResponse("/game/mai2/", 303) + self.logger.debug(f"Created session with JWT {encoded_sesh}") resp.set_cookie("ARTEMIS_SESH", encoded_sesh) return resp else: - return RedirectResponse("/gate/", 303) \ No newline at end of file + return RedirectResponse("/gate/", 303) + + async def render_events(self, request: Request) -> Response: + usr_sesh = self.validate_session(request) + if not usr_sesh: + return RedirectResponse("/gate/", 303) + + if not self.test_perm(usr_sesh.permissions, PermissionOffset.SYSADMIN): + return RedirectResponse("/game/mai2/", 303) + + template = self.environment.get_template( + "titles/mai2/templates/events/mai2_events.jinja" + ) + + incoming_ver = usr_sesh.maimai_version + evts = [] + + if incoming_ver < 0: + usr_sesh.maimai_version = Mai2Constants.VER_MAIMAI_DX + + event_list = await self.data.static.get_game_events(usr_sesh.maimai_version) + self.logger.info(f"Get events for v{usr_sesh.maimai_version}") + + for event in event_list: + evts.append({ + "id": event['id'], + "version": event['version'], + "eventId": event['eventId'], + "eventType": event['type'], + "name": event['name'], + "startDate": event['startDate'].strftime("%x %X"), + "enabled": "true" if event['enabled'] else "false", + }) + + resp = Response(template.render( + title=f"{self.core_config.server.name} | {self.nav_name} Events", + game_list=self.environment.globals["game_list"], + sesh=vars(usr_sesh), + version_list=Mai2Constants.VERSION_STRING, + events=evts + ), media_type="text/html; charset=utf-8") + + if incoming_ver < 0: + encoded_sesh = self.encode_session(usr_sesh) + resp.delete_cookie("ARTEMIS_SESH") + resp.set_cookie("ARTEMIS_SESH", encoded_sesh) + + return resp + + async def render_event_edit(self, request: Request) -> Response: + usr_sesh = self.validate_session(request) + if not usr_sesh: + return RedirectResponse("/gate/", 303) + + if not self.test_perm(usr_sesh.permissions, PermissionOffset.SYSADMIN): + return RedirectResponse("/game/mai2/", 303) + + template = self.environment.get_template( + "titles/mai2/templates/events/mai2_event_edit.jinja" + ) + + evt_id = request.path_params.get("event_id") + + event_id = await self.data.static.get_event_by_id(evt_id) + if not event_id: + return RedirectResponse("/game/mai2/events/", 303) + + return Response(template.render( + title=f"{self.core_config.server.name} | {self.nav_name} Edit Event {evt_id}", + game_list=self.environment.globals["game_list"], + sesh=vars(usr_sesh), + user_id=usr_sesh.user_id, + version_list=Mai2Constants.VERSION_STRING, + cur_version=usr_sesh.maimai_version, + event=event_id._asdict() + ), media_type="text/html; charset=utf-8") + + async def update_event(self, request: Request) -> RedirectResponse: + usr_sesh = self.validate_session(request) + if not usr_sesh: + return RedirectResponse("/gate/", 303) + + if not self.test_perm(usr_sesh.permissions, PermissionOffset.SYSADMIN): + return RedirectResponse("/game/mai2/", 303) + + form_data = await request.form() + print(form_data) + event_id: int = form_data.get("evtId", None) + new_enabled: bool = bool(form_data.get("evtEnabled", False)) + try: + new_start_date: datetime = datetime.strptime(form_data.get("evtStart", None), "%Y-%m-%dT%H:%M:%S") + except: + new_start_date = None + + print(f"{event_id} {new_enabled} {new_start_date}") + if event_id is None or new_start_date is None: + return RedirectResponse("/game/mai2/events/?e=4", 303) + + await self.data.static.update_event_by_id(int(event_id), new_enabled, new_start_date) + + return RedirectResponse("/game/mai2/events/?s=1", 303) diff --git a/titles/mai2/schema/static.py b/titles/mai2/schema/static.py index e33e4ec..ddba0f8 100644 --- a/titles/mai2/schema/static.py +++ b/titles/mai2/schema/static.py @@ -7,6 +7,7 @@ from sqlalchemy.schema import ForeignKey from sqlalchemy.sql import func, select from sqlalchemy.engine import Row from sqlalchemy.dialects.mysql import insert +from datetime import datetime event = Table( "mai2_static_event", @@ -248,3 +249,18 @@ class Mai2StaticData(BaseData): if result is None: return None return result.fetchall() + + async def get_event_by_id(self, table_id: int) -> Optional[Row]: + result = await self.execute(event.select(event.c.id == table_id)) + if result: + return result.fetchone() + + async def get_events_by_event_id(self, event_id: int) -> Optional[List[Row]]: + result = await self.execute(event.select(event.c.eventId == event_id)) + if result: + return result.fetchall() + + async def update_event_by_id(self, table_id: int, is_enable: bool, start_date: datetime) -> None: + result = await self.execute(event.update(event.c.id == table_id).values(enabled=is_enable, startDate = start_date)) + if not result: + self.logger.error(f"Failed to update event {table_id} - {is_enable} {start_date}") diff --git a/titles/mai2/templates/events/mai2_event_edit.jinja b/titles/mai2/templates/events/mai2_event_edit.jinja new file mode 100644 index 0000000..df061f5 --- /dev/null +++ b/titles/mai2/templates/events/mai2_event_edit.jinja @@ -0,0 +1,16 @@ +{% extends "core/templates/index.jinja" %} +{% block content %} + +
+

Event {{ event.eventId }} for {{ version_list[event.version] }}: {{ event.name }}

+ +
+

+ +
+

+ +   + +
+{% endblock content %} diff --git a/titles/mai2/templates/events/mai2_events.jinja b/titles/mai2/templates/events/mai2_events.jinja new file mode 100644 index 0000000..a2e9325 --- /dev/null +++ b/titles/mai2/templates/events/mai2_events.jinja @@ -0,0 +1,156 @@ +{% extends "core/templates/index.jinja" %} +{% block content %} +

Events

+
+ +
+ + + + + + + + + + + + + + + {% if events is not defined or events|length == 0 %} + + + + {% endif %} +
Viewing all events
IDVersionEvent IDEvent TypeNameStart DateEnabledActions
No Events
+
+ +  + + +
+ +{% endblock content %} \ No newline at end of file diff --git a/titles/mai2/templates/mai2_header.jinja b/titles/mai2/templates/mai2_header.jinja index 4a4cb86..f226fbe 100644 --- a/titles/mai2/templates/mai2_header.jinja +++ b/titles/mai2/templates/mai2_header.jinja @@ -3,6 +3,9 @@ \ No newline at end of file From 15cf56ed9bc857110bfaa1edfb6f8afb4f0f09fe Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Sat, 6 Jul 2024 21:59:18 -0400 Subject: [PATCH 4/7] dbutils: get loop once at top of file --- dbutils.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/dbutils.py b/dbutils.py index 955e509..21b5c9d 100644 --- a/dbutils.py +++ b/dbutils.py @@ -42,6 +42,8 @@ if __name__ == "__main__": data = Data(cfg) + loop = asyncio.get_event_loop() + if args.action == "create": data.create_database() @@ -55,19 +57,15 @@ if __name__ == "__main__": data.schema_downgrade(args.version) elif args.action == "create-owner": - loop = asyncio.get_event_loop() loop.run_until_complete(data.create_owner(args.email, args.access_code)) elif args.action == "migrate": - loop = asyncio.get_event_loop() loop.run_until_complete(data.migrate()) elif args.action == "create-revision": - loop = asyncio.get_event_loop() loop.run_until_complete(data.create_revision(args.message)) elif args.action == "create-autorevision": - loop = asyncio.get_event_loop() loop.run_until_complete(data.create_revision_auto(args.message)) else: From c65c71e89e4c713b5bbf377d97948c143bf5c16c Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Sat, 6 Jul 2024 23:13:41 -0400 Subject: [PATCH 5/7] card: add memos --- .../versions/5ea73f89d982_card_add_memo.py | 28 ++++++++++++ core/data/schema/card.py | 8 +++- core/frontend.py | 43 +++++++++++++++++-- core/templates/user/index.jinja | 14 ++++-- 4 files changed, 85 insertions(+), 8 deletions(-) create mode 100644 core/data/alembic/versions/5ea73f89d982_card_add_memo.py diff --git a/core/data/alembic/versions/5ea73f89d982_card_add_memo.py b/core/data/alembic/versions/5ea73f89d982_card_add_memo.py new file mode 100644 index 0000000..84c8a18 --- /dev/null +++ b/core/data/alembic/versions/5ea73f89d982_card_add_memo.py @@ -0,0 +1,28 @@ +"""card_add_memo + +Revision ID: 5ea73f89d982 +Revises: 745448d83696 +Create Date: 2024-07-06 22:46:56.992152 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = '5ea73f89d982' +down_revision = '745448d83696' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('aime_card', sa.Column('memo', sa.VARCHAR(length=16), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('aime_card', 'memo') + # ### end Alembic commands ### diff --git a/core/data/schema/card.py b/core/data/schema/card.py index 6205b4c..1865539 100644 --- a/core/data/schema/card.py +++ b/core/data/schema/card.py @@ -1,6 +1,6 @@ from typing import Dict, List, Optional from sqlalchemy import Table, Column, UniqueConstraint -from sqlalchemy.types import Integer, String, Boolean, TIMESTAMP, BIGINT +from sqlalchemy.types import Integer, String, Boolean, TIMESTAMP, BIGINT, VARCHAR from sqlalchemy.sql.schema import ForeignKey from sqlalchemy.sql import func from sqlalchemy.engine import Row @@ -19,6 +19,7 @@ aime_card = Table( Column("last_login_date", TIMESTAMP, onupdate=func.now()), Column("is_locked", Boolean, server_default="0"), Column("is_banned", Boolean, server_default="0"), + Column("memo", VARCHAR(16)), UniqueConstraint("user", "access_code", name="aime_card_uk"), mysql_charset="utf8mb4", ) @@ -148,6 +149,11 @@ class CardData(BaseData): if not result: self.logger.error(f"Failed to change card access code from {old_ac} to {new_ac}") + async def set_memo_by_access_code(self, access_code: str, memo: str) -> None: + result = await self.execute(aime_card.update(aime_card.c.access_code == access_code).values(memo=memo)) + if not result: + self.logger.error(f"Failed to add memo to card {access_code}") + def to_access_code(self, luid: str) -> str: """ Given a felica cards internal 16 hex character luid, convert it to a 0-padded 20 digit access code as a string diff --git a/core/frontend.py b/core/frontend.py index f15f58a..1d19bfe 100644 --- a/core/frontend.py +++ b/core/frontend.py @@ -476,10 +476,10 @@ class FE_User(FE_Base): card_data.append({ 'access_code': ac, 'status': status, - 'chip_id': "", #None if c['chip_id'] is None else f"{c['chip_id']:X}", - 'idm': "", + 'chip_id': c['chip_id'], + 'idm': c['idm'], 'type': c_type, - "memo": "" + "memo": c['memo'] }) if "e" in request.query_params: @@ -516,7 +516,42 @@ class FE_User(FE_Base): return resp async def edit_card(self, request: Request) -> RedirectResponse: - return RedirectResponse("/user/", 303) + frm = await request.form() + usr_sesh = self.validate_session(request) + if not usr_sesh or not self.test_perm(usr_sesh.permissions, PermissionOffset.USERMOD): + return RedirectResponse("/gate/", 303) + + frm = await request.form() + ac = frm.get("add_access_code", None) + if not ac: + return RedirectResponse("/user/?e=999", 303) + + card = await self.data.card.get_card_by_access_code(ac) + if not card: + return RedirectResponse("/user/?e=2", 303) + + if card['user'] != usr_sesh.user_id and not self.test_perm_minimum(usr_sesh.permissions, PermissionOffset.USERMOD): + return RedirectResponse("/user/?e=11", 303) + + if frm.get("add_memo", None): + memo = frm.get("add_memo") + if len(memo) > 16 or len(memo) == 0: + return RedirectResponse("/user/?e=4", 303) + await self.data.card.set_memo_by_access_code(ac, memo) + + if frm.get("add_felica_idm", None): + idm = frm.get('add_felica_idm') + if not all(c in string.hexdigits for c in idm): + return RedirectResponse("/user/?e=4", 303) + await self.data.card.set_idm_by_access_code(ac, idm) + + if frm.get("add_mifare_chip_id", None): + chip_id: str = frm.get('add_mifare_chip_id') + if not all(c in string.hexdigits for c in idm): + return RedirectResponse("/user/?e=4", 303) + await self.data.card.set_chip_id_by_access_code(ac, int(chip_id, 16)) + + return RedirectResponse("/user/?s=4", 303) async def add_card(self, request: Request) -> RedirectResponse: return RedirectResponse("/user/", 303) diff --git a/core/templates/user/index.jinja b/core/templates/user/index.jinja index 1b6ec1d..578702c 100644 --- a/core/templates/user/index.jinja +++ b/core/templates/user/index.jinja @@ -49,11 +49,14 @@ function prep_edit_form(access_code, chip_id, idm, card_type, u_memo) { fidm.value = idm; memo.value = u_memo; - if (card_type == "AmusementIC") { + if (access_code.startsWith("3") || access_code.startsWith("010")) { + cid.disabled = false; + fidm.disabled = true; + } else if (access_code.startsWith("5")) { cid.disabled = true; fidm.disabled = false; } else { - cid.disabled = false; + cid.disabled = true; fidm.disabled = true; } } @@ -91,9 +94,14 @@ Card added successfully
+{% if success is defined and success == 4 %} +
+Update successful +
+{% endif %}
    {% for c in cards %} -
  • {{ c.access_code }} ({{ c.type}}): {{ c.status }}  {% if c.status == 'Active'%}{% elif c.status == 'Locked' %}{% endif %} 
  • +
  • {{ c.access_code }} ({{ c.type if c.memo is none or not c.memo else c.memo }}): {{ c.status }}  {% if c.status == 'Active'%}{% elif c.status == 'Locked' %}{% endif %} 
  • {% endfor %}
From d7e8c9b4901d7f65568a711c9b5cff1c690bfa02 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Sat, 6 Jul 2024 23:20:04 -0400 Subject: [PATCH 6/7] frontend: fix event log buttons --- core/templates/sys/logs.jinja | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/core/templates/sys/logs.jinja b/core/templates/sys/logs.jinja index 9b7a987..2370f6f 100644 --- a/core/templates/sys/logs.jinja +++ b/core/templates/sys/logs.jinja @@ -46,7 +46,11 @@ var per_page = 0; var page = 0; function update_tbl() { - if (TBL_DATA.length == 0) { return; } + if (TBL_DATA.length == 0) { + document.getElementById("btn_next").disabled = true; + document.getElementById("btn_prev").disabled = true; + return; + } var tbl = document.getElementById("tbl_events"); for (var i = 0; i < per_page; i++) { @@ -183,7 +187,7 @@ function chg_page(num) { document.getElementById("btn_prev").disabled = true; return; } else if (page == 0) { - document.getElementById("btn_next").disabled = false; + document.getElementById("btn_next").disabled = TBL_DATA.length == 0; document.getElementById("btn_prev").disabled = true; } else { document.getElementById("btn_next").disabled = false; From 6a43e0eadabdd06d238dca9a07e30d2e9439ec17 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Tue, 9 Jul 2024 15:10:42 -0400 Subject: [PATCH 7/7] update keychip serial generation code --- core/const.py | 27 +++++++++-------- core/data/schema/arcade.py | 61 +++++++++++++++++++++++++++++++------- core/frontend.py | 9 ++++-- 3 files changed, 71 insertions(+), 26 deletions(-) diff --git a/core/const.py b/core/const.py index 98effb6..535a1bb 100644 --- a/core/const.py +++ b/core/const.py @@ -1,16 +1,18 @@ from enum import Enum -class MainboardPlatformCodes: - RINGEDGE = "AALE" - RINGWIDE = "AAML" - NU = "AAVE" - NUSX = "AAWE" - ALLS_UX = "ACAE" - ALLS_HX = "ACAX" +class MainboardPlatformCodes(Enum): + RINGEDGE = "AAL" + RINGEDGE2 = "AAS" + RINGWIDE = "AAM" + NU = "AAV" + NUSX = "AAW" + ALLS = "ACA" + #ALLS_UX = "ACAE" + #ALLS_HX = "ACAX" -class MainboardRevisions: +class MainboardRevisions(Enum): RINGEDGE = 1 RINGEDGE2 = 2 @@ -29,11 +31,10 @@ class MainboardRevisions: ALLS_HX2 = 12 -class KeychipPlatformsCodes: - RING = "A72E" - NU = ("A60E", "A60E", "A60E") - NUSX = ("A61X", "A69X") - ALLS = "A63E" +class KeychipPlatformsCodes(Enum): + RING = "72" + NU = ("60", "61", "69") + ALLS = "63" class AllnetCountryCode(Enum): diff --git a/core/data/schema/arcade.py b/core/data/schema/arcade.py index 680f827..3e83bc5 100644 --- a/core/data/schema/arcade.py +++ b/core/data/schema/arcade.py @@ -206,17 +206,6 @@ class ArcadeData(BaseData): return None return result.lastrowid - def format_serial( # TODO: Actual serial stuff - self, platform_code: str, platform_rev: int, serial_num: int, append: int = 8888 - ) -> str: - return f"{platform_code}{platform_rev:02d}A{serial_num:04d}{append:04d}" # 0x41 = A, 0x52 = R - - def validate_keychip_format(self, serial: str) -> bool: - if re.fullmatch(r"^A[0-9]{2}[E|X][-]?[0-9]{2}[A-HJ-NP-Z][0-9]{4}([0-9]{4})?$", serial) is None: - return False - - return True - async def get_arcade_by_name(self, name: str) -> Optional[List[Row]]: sql = arcade.select(or_(arcade.c.name.like(f"%{name}%"), arcade.c.nickname.like(f"%{name}%"))) result = await self.execute(sql) @@ -230,3 +219,53 @@ class ArcadeData(BaseData): if result is None: return None return result.fetchall() + + async def get_num_generated_keychips(self) -> Optional[int]: + result = await self.execute(select(func.count("serial LIKE 'A69A%'")).select_from(machine)) + if result: + return result.fetchone()['count_1'] + self.logger.error("Failed to count machine serials that start with A69A!") + + def format_serial( + self, platform_code: str, platform_rev: int, serial_letter: str, serial_num: int, append: int, dash: bool = False + ) -> str: + return f"{platform_code}{'-' if dash else ''}{platform_rev:02d}{serial_letter}{serial_num:04d}{append:04d}" + + def validate_keychip_format(self, serial: str) -> bool: + # For the 2nd letter, E and X are the only "real" values that have been observed + if re.fullmatch(r"^A[0-9]{2}[A-Z][-]?[0-9]{2}[A-HJ-NP-Z][0-9]{4}([0-9]{4})?$", serial) is None: + return False + + return True + + # Thanks bottersnike! + def get_keychip_suffix(self, year: int, month: int) -> str: + assert year > 1957 + assert 1 <= month <= 12 + + year -= 1957 + # Jan/Feb/Mar are from the previous tax year + if month < 4: + year -= 1 + assert year >= 1 and year <= 99 + + month = ((month - 1) + 9) % 12 # Offset so April=0 + return f"{year:02}{month // 6:01}{month % 6 + 1:01}" + + + def parse_keychip_suffix(self, suffix: str) -> tuple[int, int]: + year = int(suffix[0:2]) + half = int(suffix[2]) + assert half in (0, 1) + period = int(suffix[3]) + assert period in (1, 2, 3, 4, 5, 6) + + month = half * 6 + (period - 1) + month = ((month + 3) % 12) + 1 # Offset so Jan=1 + + # Jan/Feb/Mar are from the previous tax year + if month < 4: + year += 1 + year += 1957 + + return (year, month) diff --git a/core/frontend.py b/core/frontend.py index 1d19bfe..d070aa1 100644 --- a/core/frontend.py +++ b/core/frontend.py @@ -826,14 +826,19 @@ class FE_System(FE_Base): return RedirectResponse("/sys/?e=4", 303) if not serial: - serial = self.data.arcade.format_serial("A69E", 1, random.randint(1, 9999)) + append = self.data.arcade.get_keychip_suffix(datetime.now().year, datetime.now().month) + generated = await self.data.arcade.get_num_generated_keychips() + if not generated: + generated = 0 + serial = self.data.arcade.format_serial("A69A", 1, "A", generated + 1, int(append)) + serial_dash = self.data.arcade.format_serial("A69A", 1, "A", generated + 1, int(append), True) cab_id = await self.data.arcade.create_machine(int(shopid), serial, None, game_code if game_code else None) return Response(template.render( title=f"{self.core_config.server.name} | System", sesh=vars(usr_sesh), - cabadd={"id": cab_id, "serial": serial}, + cabadd={"id": cab_id, "serial": serial_dash}, ), media_type="text/html; charset=utf-8") async def render_logs(self, request: Request):