allnet: basic playhistory

This commit is contained in:
2025-04-24 23:04:01 -04:00
parent ce475e801b
commit eea9ca21ca
3 changed files with 133 additions and 7 deletions

View File

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

View File

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

View File

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