From eea9ca21ca83505b9dd71a9e8708c50adce0b834 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Thu, 24 Apr 2025 23:04:01 -0400 Subject: [PATCH] allnet: basic playhistory --- core/allnet.py | 32 ++++++++-- .../f6007bbf057d_add_billing_playcount.py | 50 ++++++++++++++++ core/data/schema/arcade.py | 58 ++++++++++++++++++- 3 files changed, 133 insertions(+), 7 deletions(-) create mode 100644 core/data/alembic/versions/f6007bbf057d_add_billing_playcount.py diff --git a/core/allnet.py b/core/allnet.py index 2cb823e..cd349f7 100644 --- a/core/allnet.py +++ b/core/allnet.py @@ -593,7 +593,6 @@ class BillingServlet: return PlainTextResponse("result=5&linelimit=&message=field is missing or formatting is incorrect\r\n") kc_serial_bytes = req.keychipid.encode() - machine = await self.data.arcade.get_machine(req.keychipid) if machine is None and not self.config.server.allow_unregistered_serials: @@ -614,6 +613,32 @@ class BillingServlet: } if machine is not None: + if self.config.allnet.save_billing: + lastcredit = await self.data.arcade.billing_get_credit(machine['id'], req.gameid) + if lastcredit is not None: + last_playct = lastcredit['playcount'] + else: + last_playct = 0 + + # Technically if a cab resets it's playcount and then does more plays then the previous + # playcount before a billing checkin occours, we will lose plays equal to the current playcount. + if req.playcnt < last_playct: await self.data.arcade.billing_add_playcount(machine['id'], req.gameid, req.playcnt) + elif req.playcnt == last_playct: pass # No plays since last checkin, skip update + else: await self.data.arcade.billing_add_playcount(machine['id'], req.gameid, req.playcnt - last_playct) + + plays = await self.data.arcade.billing_get_playcount_3mo(machine['id'], req.gameid) + if plays is not None and len(plays) > 0: + playhist = "" + + for x in range(len(plays), 0, -1): playhist += f"{plays[x]['year']:04d}{plays[x]['month']:02d}/{plays[x]['playct']}:" + playhist = playhist[:-1] + + else: + playhist = "000000/0:000000/0:000000/0" + + else: + playhist = "000000/0:000000/0:000000/0" + for x in range(1, len(req_dict)): if not req_dict[x]: continue @@ -649,6 +674,7 @@ class BillingServlet: if self.config.allnet.save_billing: await self.data.arcade.billing_set_credit( machine['id'], + req.gameid, tmp.chute_type.value, tmp.service_type.value, tmp.operation_type.value, @@ -708,9 +734,7 @@ class BillingServlet: digest.update(nearfull.to_bytes(4, "little") + kc_serial_bytes) nearfull_sig = signer.sign(digest).hex() - # TODO: playhistory - - resp = BillingResponse(playlimit, playlimit_sig, nearfull, nearfull_sig, req.requestno, req.protocolver) + resp = BillingResponse(playlimit, playlimit_sig, nearfull, nearfull_sig, req.requestno, req.protocolver, playhist) resp_str = urllib.parse.unquote(urllib.parse.urlencode(vars(resp))) + "\r\n" diff --git a/core/data/alembic/versions/f6007bbf057d_add_billing_playcount.py b/core/data/alembic/versions/f6007bbf057d_add_billing_playcount.py new file mode 100644 index 0000000..e6a5259 --- /dev/null +++ b/core/data/alembic/versions/f6007bbf057d_add_billing_playcount.py @@ -0,0 +1,50 @@ +"""add_billing_playcount + +Revision ID: f6007bbf057d +Revises: 27e3434740df +Create Date: 2025-04-19 18:20:35.554137 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = 'f6007bbf057d' +down_revision = '27e3434740df' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('machine_billing_playcount', + sa.Column('id', sa.BIGINT(), nullable=False), + sa.Column('machine', sa.Integer(), nullable=False), + sa.Column('game_id', sa.CHAR(length=5), nullable=False), + sa.Column('year', sa.INTEGER(), nullable=False), + sa.Column('month', sa.INTEGER(), nullable=False), + sa.Column('playct', sa.BIGINT(), server_default='1', nullable=False), + sa.ForeignKeyConstraint(['machine'], ['machine.id'], onupdate='cascade', ondelete='cascade'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('machine'), + sa.UniqueConstraint('machine', 'game_id', 'year', 'month', name='machine_billing_playcount_uk'), + mysql_charset='utf8mb4' + ) + op.add_column('machine_billing_credit', sa.Column('game_id', sa.CHAR(length=5), nullable=False)) + op.drop_constraint("machine_billing_credit_ibfk_1", "machine_billing_credit", "foreignkey") + op.drop_index('machine', table_name='machine_billing_credit') + op.create_unique_constraint('machine_billing_credit_uk', 'machine_billing_credit', ['machine', 'game_id']) + op.create_foreign_key("machine_billing_credit_ibfk_1", "machine_billing_credit", "machine", ["machine"], ["id"], onupdate='cascade', ondelete='cascade') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint("machine_billing_credit_ibfk_1", "machine_billing_credit", "foreignkey") + op.drop_constraint('machine_billing_credit_uk', 'machine_billing_credit', type_='unique') + op.create_index('machine', 'machine_billing_credit', ['machine'], unique=True) + op.create_foreign_key("machine_billing_credit_ibfk_1", "machine_billing_credit", "machine", ["machine"], ["id"], onupdate='cascade', ondelete='cascade') + op.drop_column('machine_billing_credit', 'game_id') + op.drop_table('machine_billing_playcount') + # ### end Alembic commands ### diff --git a/core/data/schema/arcade.py b/core/data/schema/arcade.py index cfbff96..2053f62 100644 --- a/core/data/schema/arcade.py +++ b/core/data/schema/arcade.py @@ -1,7 +1,8 @@ import re from typing import List, Optional +from datetime import datetime -from sqlalchemy import Column, Table, and_, or_ +from sqlalchemy import Column, Table, and_, or_, UniqueConstraint from sqlalchemy.dialects.mysql import insert from sqlalchemy.engine import Row from sqlalchemy.sql import func, select @@ -98,8 +99,9 @@ billing_credit: Table = Table( "machine", Integer, ForeignKey("machine.id", ondelete="cascade", onupdate="cascade"), - nullable=False, unique=True + nullable=False ), + Column("game_id", CHAR(5), nullable=False), Column("chute_type", INTEGER, nullable=False), Column("service_type", INTEGER, nullable=False), Column("operation_type", INTEGER, nullable=False), @@ -115,6 +117,25 @@ billing_credit: Table = Table( Column("coin_count_slot5", INTEGER, nullable=False), Column("coin_count_slot6", INTEGER, nullable=False), Column("coin_count_slot7", INTEGER, nullable=False), + UniqueConstraint("machine", "game_id", name="machine_billing_credit_uk"), + mysql_charset="utf8mb4", +) + +billing_playct: Table = Table( + "machine_billing_playcount", + metadata, + Column("id", BIGINT, primary_key=True, nullable=False), + Column( + "machine", + Integer, + ForeignKey("machine.id", ondelete="cascade", onupdate="cascade"), + nullable=False, unique=True + ), + Column("game_id", CHAR(5), nullable=False), + Column("year", INTEGER, nullable=False), + Column("month", INTEGER, nullable=False), + Column("playct", BIGINT, nullable=False, server_default="1"), + UniqueConstraint("machine", "game_id", "year", "month", name="machine_billing_playcount_uk"), mysql_charset="utf8mb4", ) @@ -413,12 +434,13 @@ class ArcadeData(BaseData): return None return result.lastrowid - async def billing_set_credit(self, machine_id: int, chute_type: int, service_type: int, op_mode: int, coin_rate0: int, coin_rate1: int, + async def billing_set_credit(self, machine_id: int, game_id: str, chute_type: int, service_type: int, op_mode: int, coin_rate0: int, coin_rate1: int, bonus_adder: int, coin_to_credit_rate: int, coin_count_slot0: int, coin_count_slot1: int, coin_count_slot2: int, coin_count_slot3: int, coin_count_slot4: int, coin_count_slot5: int, coin_count_slot6: int, coin_count_slot7: int) -> Optional[int]: sql = insert(billing_credit).values( machine=machine_id, + game_id=game_id, chute_type=chute_type, service_type=service_type, operation_type=op_mode, @@ -460,6 +482,36 @@ class ArcadeData(BaseData): return None return result.lastrowid + async def billing_get_credit(self, machine_id: int, game_id: str) -> Optional[Row]: + result = await self.execute(billing_credit.select(billing_credit.c.machine == machine_id)) + if result: + return result.fetchone() + + async def billing_add_playcount(self, machine_id: int, game_id: str, playct: int = 1) -> None: + now = datetime.now() + sql = insert(billing_playct).values( + machine=machine_id, + game_id=game_id, + year=now.year, + month=now.month, + playct=playct + ) + + conflict = sql.on_duplicate_key_update(playct=billing_playct.c.playct + playct) + result = await self.execute(conflict) + + 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, + billing_playct.c.game_id == game_id + )).order_by(billing_playct.c.year.desc(), billing_playct.c.month.desc()).limit(3)) + + if result is not None: + return result.fetchall() + def format_serial( self, platform_code: str, platform_rev: int, serial_letter: str, serial_num: int, append: int, dash: bool = False ) -> str: