1
0
mirror of synced 2025-01-18 14:14:03 +01:00

Move logic for calculating play statistics into common module and backend base module.

This commit is contained in:
Jennifer Taylor 2021-08-24 23:18:53 +00:00
parent bcfd4fcf24
commit bb7916d3c4
27 changed files with 520 additions and 313 deletions

View File

@ -2,7 +2,7 @@ from abc import ABC
import traceback
from typing import Any, Dict, Iterator, List, Optional, Set, Tuple, Type
from bemani.common import Model, ValidatedDict, Profile, GameConstants, Time
from bemani.common import Model, ValidatedDict, Profile, PlayStatistics, GameConstants, Time
from bemani.data import Config, Data, UserID, RemoteUser
@ -318,7 +318,7 @@ class Base(ABC):
raise Exception('Trying to save a remote profile locally!')
self.data.local.user.put_profile(self.game, self.version, userid, profile)
def update_play_statistics(self, userid: UserID, extra_stats: Optional[Dict[str, Any]]=None) -> None:
def update_play_statistics(self, userid: UserID, stats: Optional[PlayStatistics] = None) -> None:
"""
Given a user ID, calculate new play statistics.
@ -327,18 +327,19 @@ class Base(ABC):
Parameters:
userid - The user ID we are binding the profile for.
stats - A play statistics object we should store extra data from.
"""
if RemoteUser.is_remote(userid):
raise Exception('Trying to save remote statistics locally!')
# We store the play statistics in a series-wide settings blob so its available
# across all game versions, since it isn't game-specific.
settings = self.get_play_statistics(userid)
settings = self.data.local.game.get_settings(self.game, userid) or ValidatedDict({})
if extra_stats is not None:
for key in extra_stats:
if stats is not None:
for key in stats:
# Make sure we don't override anything we manage here
if key in [
if key in {
'total_plays',
'today_plays',
'total_days',
@ -346,14 +347,14 @@ class Base(ABC):
'last_play_timestamp',
'last_play_date',
'consecutive_days',
]:
}:
continue
# Safe to copy over
settings[key] = extra_stats[key]
settings[key] = stats[key]
settings.replace_int('total_plays', settings.get_int('total_plays') + 1)
settings.replace_int('first_play_timestamp', settings.get_int('first_play_timestamp', int(Time.now())))
settings.replace_int('last_play_timestamp', int(Time.now()))
settings.replace_int('first_play_timestamp', settings.get_int('first_play_timestamp', Time.now()))
settings.replace_int('last_play_timestamp', Time.now())
last_play_date = settings.get_int_array('last_play_date', 3)
today_play_date = Time.todays_date()
@ -363,13 +364,13 @@ class Base(ABC):
last_play_date[1] == today_play_date[1] and
last_play_date[2] == today_play_date[2]
):
# We already played today, add one
# We already played today, add one.
settings.replace_int('today_plays', settings.get_int('today_plays') + 1)
else:
# We played on a new day, so count total days up
# We played on a new day, so count total days up.
settings.replace_int('total_days', settings.get_int('total_days') + 1)
# We haven't played yet today, reset to one
# We played only once today (the play we are saving).
settings.replace_int('today_plays', 1)
if (
last_play_date[0] == yesterday_play_date[0] and
@ -379,7 +380,7 @@ class Base(ABC):
# We played yesterday, add one to consecutive days
settings.replace_int('consecutive_days', settings.get_int('consecutive_days') + 1)
else:
# We haven't played yet today or yesterday, reset consecutive days
# We haven't played yesterday, so we have only one consecutive day.
settings.replace_int('consecutive_days', 1)
settings.replace_int_array('last_play_date', 3, today_play_date)
@ -413,7 +414,7 @@ class Base(ABC):
settings = ValidatedDict()
return settings
def get_play_statistics(self, userid: UserID) -> ValidatedDict:
def get_play_statistics(self, userid: UserID) -> PlayStatistics:
"""
Given a user ID, get the play statistics.
@ -433,8 +434,93 @@ class Base(ABC):
consecutive_days - Number of consecutive days played at this time.
"""
if RemoteUser.is_remote(userid):
return ValidatedDict({})
return PlayStatistics(
self.game,
0,
0,
0,
0,
Time.now(),
Time.now(),
)
# Grab the last saved settings and today's date.
settings = self.data.local.game.get_settings(self.game, userid)
today_play_date = Time.todays_date()
yesterday_play_date = Time.yesterdays_date()
if settings is None:
return ValidatedDict({})
return settings
return PlayStatistics(
self.game,
1,
1,
1,
1,
Time.now(),
Time.now(),
)
# Calculate whether we are on our first play of the day or not.
last_play_date = settings.get_int_array('last_play_date', 3)
if (
last_play_date[0] == today_play_date[0] and
last_play_date[1] == today_play_date[1] and
last_play_date[2] == today_play_date[2]
):
# We last played today, so the total days and today plays are accurate
# as stored.
today_count = settings.get_int('today_plays', 0)
total_days = settings.get_int('total_days', 1)
consecutive_days = settings.get_int('consecutive_days', 1)
else:
if (
last_play_date[0] != 0 and
last_play_date[1] != 0 and
last_play_date[2] != 0
):
# We've played before but not today, so the total days is
# the stored count plus today.
total_days = settings.get_int('total_days') + 1
else:
# We've never played before, so the total days is just 1.
total_days = 1
if (
last_play_date[0] == yesterday_play_date[0] and
last_play_date[1] == yesterday_play_date[1] and
last_play_date[2] == yesterday_play_date[2]
):
# We've played before, and it was yesterday, so today is the
# next consecutive day. So add the current value and today.
consecutive_days = settings.get_int('consecutive_days') + 1
else:
# This is the first consecutive day, we've either never played
# or we played a bunch but in the past before yesterday.
consecutive_days = 1
# We haven't played yet today.
today_count = 0
# Grab any extra settings that a game may have stored here.
extra_settings: Dict[str, Any] = {
key: value for (key, value) in settings.items()
if key not in {
'total_plays',
'today_plays',
'total_days',
'first_play_timestamp',
'last_play_timestamp',
'last_play_date',
'consecutive_days',
}
}
return PlayStatistics(
self.game,
settings.get_int('total_plays') + 1,
today_count + 1,
total_days,
consecutive_days,
settings.get_int('first_play_timestamp', Time.now()),
settings.get_int('last_play_timestamp', Time.now()),
extra_settings,
)

View File

@ -1,7 +1,7 @@
from typing import Dict, Optional, Tuple
from bemani.backend.ddr.base import DDRBase
from bemani.common import Time, Profile, intish
from bemani.common import Profile, intish
from bemani.data import Score, UserID
from bemani.protocol import Node
@ -272,19 +272,9 @@ class DDRGameLoadDailyHandler(DDRBase):
play_stats = self.get_play_statistics(userid)
# Day play counts
last_play_date = play_stats.get_int_array('last_play_date', 3)
today_play_date = Time.todays_date()
if (
last_play_date[0] == today_play_date[0] and
last_play_date[1] == today_play_date[1] and
last_play_date[2] == today_play_date[2]
):
today_count = play_stats.get_int('today_plays', 0)
else:
today_count = 0
daycount = Node.void('daycount')
game.add_child(daycount)
daycount.set_attribute('playcount', str(today_count))
daycount.set_attribute('playcount', str(play_stats.today_plays))
# Daily combo stuff, unclear how this works
dailycombo = Node.void('dailycombo')

View File

@ -333,19 +333,9 @@ class DDR2013(
workout.set_attribute('disp', '1')
# Daily play counts
last_play_date = play_stats.get_int_array('last_play_date', 3)
today_play_date = Time.todays_date()
if (
last_play_date[0] == today_play_date[0] and
last_play_date[1] == today_play_date[1] and
last_play_date[2] == today_play_date[2]
):
today_count = play_stats.get_int('today_plays', 0)
else:
today_count = 0
daycount = Node.void('daycount')
root.add_child(daycount)
daycount.set_attribute('playcount', str(today_count))
daycount.set_attribute('playcount', str(play_stats.today_plays))
# Daily combo stuff, unknown how this works
dailycombo = Node.void('dailycombo')

View File

@ -388,19 +388,9 @@ class DDR2014(
totalcalorie.set_attribute('total', str(total))
# Daily play counts
last_play_date = play_stats.get_int_array('last_play_date', 3)
today_play_date = Time.todays_date()
if (
last_play_date[0] == today_play_date[0] and
last_play_date[1] == today_play_date[1] and
last_play_date[2] == today_play_date[2]
):
today_count = play_stats.get_int('today_plays', 0)
else:
today_count = 0
daycount = Node.void('daycount')
root.add_child(daycount)
daycount.set_attribute('playcount', str(today_count))
daycount.set_attribute('playcount', str(play_stats.today_plays))
# Daily combo stuff, unknown how this works
dailycombo = Node.void('dailycombo')

View File

@ -408,19 +408,9 @@ class DDRX3(
workout.set_attribute('disp', '1')
# Daily play counts
last_play_date = play_stats.get_int_array('last_play_date', 3)
today_play_date = Time.todays_date()
if (
last_play_date[0] == today_play_date[0] and
last_play_date[1] == today_play_date[1] and
last_play_date[2] == today_play_date[2]
):
today_count = play_stats.get_int('today_plays', 0)
else:
today_count = 0
daycount = Node.void('daycount')
root.add_child(daycount)
daycount.set_attribute('playcount', str(today_count))
daycount.set_attribute('playcount', str(play_stats.today_plays))
# Daily combo stuff, unknown how this works
dailycombo = Node.void('dailycombo')

View File

@ -12,7 +12,7 @@ from bemani.backend.museca.common import (
MusecaGameSaveMusicHandler,
MusecaGameShopHandler,
)
from bemani.common import Time, VersionConstants, Profile, ID
from bemani.common import VersionConstants, Profile, ID
from bemani.data import UserID
from bemani.protocol import Node
@ -215,19 +215,9 @@ class Museca1(
# Play statistics
statistics = self.get_play_statistics(userid)
last_play_date = statistics.get_int_array('last_play_date', 3)
today_play_date = Time.todays_date()
if (
last_play_date[0] == today_play_date[0] and
last_play_date[1] == today_play_date[1] and
last_play_date[2] == today_play_date[2]
):
today_count = statistics.get_int('today_plays', 0)
else:
today_count = 0
game.add_child(Node.u32('play_count', statistics.get_int('total_plays', 0)))
game.add_child(Node.u32('daily_count', today_count))
game.add_child(Node.u32('play_chain', statistics.get_int('consecutive_days', 0)))
game.add_child(Node.u32('play_count', statistics.total_plays))
game.add_child(Node.u32('daily_count', statistics.today_plays))
game.add_child(Node.u32('play_chain', statistics.consecutive_days))
# Last played stuff
if 'last' in profile:

View File

@ -13,7 +13,7 @@ from bemani.backend.museca.common import (
MusecaGameShopHandler,
)
from bemani.backend.museca.museca1 import Museca1
from bemani.common import Time, VersionConstants, Profile, ID
from bemani.common import VersionConstants, Profile, ID
from bemani.data import UserID
from bemani.protocol import Node
@ -354,19 +354,9 @@ class Museca1Plus(
# Play statistics
statistics = self.get_play_statistics(userid)
last_play_date = statistics.get_int_array('last_play_date', 3)
today_play_date = Time.todays_date()
if (
last_play_date[0] == today_play_date[0] and
last_play_date[1] == today_play_date[1] and
last_play_date[2] == today_play_date[2]
):
today_count = statistics.get_int('today_plays', 0)
else:
today_count = 0
game.add_child(Node.u32('play_count', statistics.get_int('total_plays', 0)))
game.add_child(Node.u32('daily_count', today_count))
game.add_child(Node.u32('play_chain', statistics.get_int('consecutive_days', 0)))
game.add_child(Node.u32('play_count', statistics.total_plays))
game.add_child(Node.u32('daily_count', statistics.today_plays))
game.add_child(Node.u32('play_chain', statistics.consecutive_days))
# Last played stuff
if 'last' in profile:

View File

@ -6,7 +6,7 @@ from typing import Dict, List, Optional
from bemani.backend.popn.base import PopnMusicBase
from bemani.backend.popn.lapistoria import PopnMusicLapistoria
from bemani.common import Time, Profile, VersionConstants
from bemani.common import Profile, VersionConstants
from bemani.data import UserID, Link
from bemani.protocol import Node
@ -546,20 +546,10 @@ class PopnMusicEclale(PopnMusicBase):
# player statistics
statistics = self.get_play_statistics(userid)
last_play_date = statistics.get_int_array('last_play_date', 3)
today_play_date = Time.todays_date()
if (
last_play_date[0] == today_play_date[0] and
last_play_date[1] == today_play_date[1] and
last_play_date[2] == today_play_date[2]
):
today_count = statistics.get_int('today_plays', 0)
else:
today_count = 0
account.add_child(Node.s16('total_play_cnt', statistics.get_int('total_plays', 0)))
account.add_child(Node.s16('today_play_cnt', today_count))
account.add_child(Node.s16('consecutive_days', statistics.get_int('consecutive_days', 0)))
account.add_child(Node.s16('total_days', statistics.get_int('total_days', 0)))
account.add_child(Node.s16('total_play_cnt', statistics.total_plays))
account.add_child(Node.s16('today_play_cnt', statistics.today_plays))
account.add_child(Node.s16('consecutive_days', statistics.consecutive_days))
account.add_child(Node.s16('total_days', statistics.total_days))
account.add_child(Node.s16('interval_day', 0))
# Set up info node

View File

@ -6,7 +6,7 @@ from bemani.backend.popn.base import PopnMusicBase
from bemani.backend.popn.tunestreet import PopnMusicTuneStreet
from bemani.backend.base import Status
from bemani.common import Profile, VersionConstants, Time, ID
from bemani.common import Profile, VersionConstants, ID
from bemani.data import Score, Link, UserID
from bemani.protocol import Node
@ -120,19 +120,9 @@ class PopnMusicFantasia(PopnMusicBase):
# Statistics section and scores section
statistics = self.get_play_statistics(userid)
last_play_date = statistics.get_int_array('last_play_date', 3)
today_play_date = Time.todays_date()
if (
last_play_date[0] == today_play_date[0] and
last_play_date[1] == today_play_date[1] and
last_play_date[2] == today_play_date[2]
):
today_count = statistics.get_int('today_plays', 0)
else:
today_count = 0
base.add_child(Node.s32('total_play_cnt', statistics.get_int('total_plays', 0)))
base.add_child(Node.s16('today_play_cnt', today_count))
base.add_child(Node.s16('consecutive_days', statistics.get_int('consecutive_days', 0)))
base.add_child(Node.s32('total_play_cnt', statistics.total_plays))
base.add_child(Node.s16('today_play_cnt', statistics.today_plays))
base.add_child(Node.s16('consecutive_days', statistics.consecutive_days))
# Number of rivals that are active for this version.
links = self.data.local.user.get_links(self.game, self.version, userid)

View File

@ -6,7 +6,7 @@ from bemani.backend.popn.base import PopnMusicBase
from bemani.backend.popn.sunnypark import PopnMusicSunnyPark
from bemani.backend.base import Status
from bemani.common import Profile, VersionConstants, Time, ID
from bemani.common import Profile, VersionConstants, ID
from bemani.data import UserID, Link
from bemani.protocol import Node
@ -340,20 +340,10 @@ class PopnMusicLapistoria(PopnMusicBase):
# Statistics section and scores section
statistics = self.get_play_statistics(userid)
last_play_date = statistics.get_int_array('last_play_date', 3)
today_play_date = Time.todays_date()
if (
last_play_date[0] == today_play_date[0] and
last_play_date[1] == today_play_date[1] and
last_play_date[2] == today_play_date[2]
):
today_count = statistics.get_int('today_plays', 0)
else:
today_count = 0
account.add_child(Node.s16('total_play_cnt', statistics.get_int('total_plays', 0)))
account.add_child(Node.s16('today_play_cnt', today_count))
account.add_child(Node.s16('consecutive_days', statistics.get_int('consecutive_days', 0)))
account.add_child(Node.s16('total_days', statistics.get_int('total_days', 0)))
account.add_child(Node.s16('total_play_cnt', statistics.total_plays))
account.add_child(Node.s16('today_play_cnt', statistics.today_plays))
account.add_child(Node.s16('consecutive_days', statistics.consecutive_days))
account.add_child(Node.s16('total_days', statistics.total_days))
account.add_child(Node.s16('interval_day', 0))
# Number of rivals that are active for this version.

View File

@ -6,7 +6,7 @@ from bemani.backend.popn.base import PopnMusicBase
from bemani.backend.popn.fantasia import PopnMusicFantasia
from bemani.backend.base import Status
from bemani.common import Profile, VersionConstants, Time, ID
from bemani.common import Profile, VersionConstants, ID
from bemani.data import UserID, Link
from bemani.protocol import Node
@ -81,19 +81,9 @@ class PopnMusicSunnyPark(PopnMusicBase):
# Statistics section and scores section
statistics = self.get_play_statistics(userid)
last_play_date = statistics.get_int_array('last_play_date', 3)
today_play_date = Time.todays_date()
if (
last_play_date[0] == today_play_date[0] and
last_play_date[1] == today_play_date[1] and
last_play_date[2] == today_play_date[2]
):
today_count = statistics.get_int('today_plays', 0)
else:
today_count = 0
base.add_child(Node.s32('total_play_cnt', statistics.get_int('total_plays', 0)))
base.add_child(Node.s16('today_play_cnt', today_count))
base.add_child(Node.s16('consecutive_days', statistics.get_int('consecutive_days', 0)))
base.add_child(Node.s32('total_play_cnt', statistics.total_plays))
base.add_child(Node.s16('today_play_cnt', statistics.today_plays))
base.add_child(Node.s16('consecutive_days', statistics.consecutive_days))
# Number of rivals that are active for this version.
links = self.data.local.user.get_links(self.game, self.version, userid)

View File

@ -796,7 +796,6 @@ class PopnMusicUsaNeko(PopnMusicBase):
root.add_child(account)
account.add_child(Node.string('g_pm_id', self.format_extid(profile.extid)))
account.add_child(Node.string('name', profile.get_str('name', 'なし')))
account.add_child(Node.s16('tutorial', profile.get_int('tutorial')))
account.add_child(Node.s16('area_id', profile.get_int('area_id')))
account.add_child(Node.s16('use_navi', profile.get_int('use_navi')))
account.add_child(Node.s16('read_news', profile.get_int('read_news')))
@ -817,6 +816,53 @@ class PopnMusicUsaNeko(PopnMusicBase):
account.add_child(Node.s32('player_point', profile.get_int('player_point', 300)))
account.add_child(Node.s32_array('power_point_list', profile.get_int_array('power_point_list', 20, [-1] * 20)))
# Tutorial handling is all sorts of crazy in UsaNeko. the tutorial flag
# is split into two values. The game uses the flag modulo 100 for standard
# tutorial progress, and the flag divided by 100 for the hold note tutorial.
# The hold note tutorial will activate the first time you choose a song with
# hold notes in it, regardless of whether you say yes/no. The total times you
# have ever played Pop'n Music also factors in for some screens. The enumerated
# values are as follows:
#
# Lower values:
# 0 - Should not be used, presenting this to the game causes buggy behavior.
# 1 - User has not been prompted to choose any tutorials. Prompts the user for the
# menu tutorial. If the user selects "no" then moves the tutorial state to
# "2" at the end of the round. If the user selects "yes" then moves the
# tutorial state to "3" immediately and starts the menu tutorial. If the total
# play count for this user is "1" when this value is hit, the game will bug
# out and play the hold note tutorial and then crash.
# 2 - Prompt the user on the mode select screen asking them if they want to see
# the menu tutorial. If the user selects "no" then moves the tutorial state
# to "8" immediately. If the user selects "yes" then moves the tutorial state
# to "3" immediately. If the total play count for this user is "1" when this value
# is hit, then the game will bug out and play the hold note tutorial and then crash.
# 3 - Display some tutorial elements on most screens, and then advance the tutorial
# state to "4" on profile save.
# 4 - Display some tutorial elements on most screens, and then advance the tutorial
# state to "5" on profile save.
# 5 - Display some tutorial elements on most screens, and then prompt user with a
# repeat tutorial question. If the user selects "no" then moves the tutorial
# state to "8". If the user selects "yes" then moves the tutorial state to "3".
# 6 - Do nothing, display nothing, but advance the tutorial state to "7" at the
# end of the game. It seems that nothing requests this state.
# 7 - Display guide information prompt on the option select screen. Game moves
# this to "8" after this tutorial has been displayed.
# 8 - Do not display any more tutorial stuff, this is a terminal state.
#
# Upper values:
# 0 - Should not be used, presenting this to the game causes buggy behavior.
# 1 - Hold note tutorial has not been activated yet and will be displayed when
# the player chooses a song with hold notes. Game moves this to "2" after this
# tutorial has been activated.
# 2 - Hold note tutorial was displayed to the user, but the mini-tutorial showing
# the hold note indicator that pops up after the hold note tutorial has not
# been displayed yet. Presumably this is just in case you play a hold note
# song on your last stage. Game moves this to "3" after this tutorial has been
# displayed.
# 3 - All hold note tutorials are finished, this is a terminal state.
account.add_child(Node.s16('tutorial', profile.get_int('tutorial')))
# Stuff we never change
account.add_child(Node.s8('staff', 0))
account.add_child(Node.s16('item_type', 0))
@ -837,20 +883,10 @@ class PopnMusicUsaNeko(PopnMusicBase):
# Player statistics
statistics = self.get_play_statistics(userid)
last_play_date = statistics.get_int_array('last_play_date', 3)
today_play_date = Time.todays_date()
if (
last_play_date[0] == today_play_date[0] and
last_play_date[1] == today_play_date[1] and
last_play_date[2] == today_play_date[2]
):
today_count = statistics.get_int('today_plays', 0)
else:
today_count = 0
account.add_child(Node.s16('total_play_cnt', statistics.get_int('total_plays', 0)))
account.add_child(Node.s16('today_play_cnt', today_count))
account.add_child(Node.s16('consecutive_days', statistics.get_int('consecutive_days', 0)))
account.add_child(Node.s16('total_days', statistics.get_int('total_days', 0)))
account.add_child(Node.s16('total_play_cnt', statistics.total_plays))
account.add_child(Node.s16('today_play_cnt', statistics.today_plays))
account.add_child(Node.s16('consecutive_days', statistics.consecutive_days))
account.add_child(Node.s16('total_days', statistics.total_days))
account.add_child(Node.s16('interval_day', 0))
# Number of rivals that are active for this version.

View File

@ -705,26 +705,14 @@ class ReflecBeatColette(ReflecBeatBase):
pdata = Node.void('pdata')
root.add_child(pdata)
# Account time info
last_play_date = statistics.get_int_array('last_play_date', 3)
today_play_date = Time.todays_date()
if (
last_play_date[0] == today_play_date[0] and
last_play_date[1] == today_play_date[1] and
last_play_date[2] == today_play_date[2]
):
today_count = statistics.get_int('today_plays', 0)
else:
today_count = 0
account = Node.void('account')
pdata.add_child(account)
account.add_child(Node.s32('usrid', profile.extid))
account.add_child(Node.s32('tpc', statistics.get_int('total_plays', 0)))
account.add_child(Node.s32('dpc', today_count))
account.add_child(Node.s32('tpc', statistics.total_plays))
account.add_child(Node.s32('dpc', statistics.today_plays))
account.add_child(Node.s32('crd', 1))
account.add_child(Node.s32('brd', 1))
account.add_child(Node.s32('tdc', statistics.get_int('total_days', 0)))
account.add_child(Node.s32('tdc', statistics.total_days))
account.add_child(Node.s32('intrvld', 0))
account.add_child(Node.s16('ver', 5))
account.add_child(Node.u64('pst', 0))

View File

@ -927,27 +927,15 @@ class ReflecBeatGroovin(ReflecBeatBase):
pdata = Node.void('pdata')
root.add_child(pdata)
# Account time info
last_play_date = statistics.get_int_array('last_play_date', 3)
today_play_date = Time.todays_date()
if (
last_play_date[0] == today_play_date[0] and
last_play_date[1] == today_play_date[1] and
last_play_date[2] == today_play_date[2]
):
today_count = statistics.get_int('today_plays', 0)
else:
today_count = 0
# Account info
account = Node.void('account')
pdata.add_child(account)
account.add_child(Node.s32('usrid', profile.extid))
account.add_child(Node.s32('tpc', statistics.get_int('total_plays', 0)))
account.add_child(Node.s32('dpc', today_count))
account.add_child(Node.s32('tpc', statistics.total_plays))
account.add_child(Node.s32('dpc', statistics.today_plays))
account.add_child(Node.s32('crd', 1))
account.add_child(Node.s32('brd', 1))
account.add_child(Node.s32('tdc', statistics.get_int('total_days', 0)))
account.add_child(Node.s32('tdc', statistics.total_days))
account.add_child(Node.s32('intrvld', 0))
account.add_child(Node.s16('ver', 1))
account.add_child(Node.u64('pst', 0))

View File

@ -563,23 +563,12 @@ class ReflecBeatLimelight(ReflecBeatBase):
base.add_child(Node.s32('pc', profile.get_int('pc')))
base.add_child(Node.s32('uattr', profile.get_int('uattr')))
last_play_date = statistics.get_int_array('last_play_date', 3)
today_play_date = Time.todays_date()
if (
last_play_date[0] == today_play_date[0] and
last_play_date[1] == today_play_date[1] and
last_play_date[2] == today_play_date[2]
):
today_count = statistics.get_int('today_plays', 0)
else:
today_count = 0
con = Node.void('con')
pdata.add_child(con)
con.add_child(Node.s32('day', today_count))
con.add_child(Node.s32('cnt', statistics.get_int('total_plays')))
con.add_child(Node.s32('total_cnt', statistics.get_int('total_plays')))
con.add_child(Node.s32('last', statistics.get_int('last_play_timestamp')))
con.add_child(Node.s32('day', statistics.today_plays))
con.add_child(Node.s32('cnt', statistics.total_plays))
con.add_child(Node.s32('total_cnt', statistics.total_plays))
con.add_child(Node.s32('last', statistics.last_play_timestamp))
con.add_child(Node.s32('now', Time.now()))
team = Node.void('team')

View File

@ -321,22 +321,11 @@ class ReflecBeat(ReflecBeatBase):
base.add_child(Node.s16('ap', profile.get_int('ap')))
base.add_child(Node.s32('flag', profile.get_int('flag')))
last_play_date = statistics.get_int_array('last_play_date', 3)
today_play_date = Time.todays_date()
if (
last_play_date[0] == today_play_date[0] and
last_play_date[1] == today_play_date[1] and
last_play_date[2] == today_play_date[2]
):
today_count = statistics.get_int('today_plays', 0)
else:
today_count = 0
con = Node.void('con')
pdata.add_child(con)
con.add_child(Node.s32('day', today_count))
con.add_child(Node.s32('cnt', statistics.get_int('total_plays')))
con.add_child(Node.s32('last', statistics.get_int('last_play_timestamp')))
con.add_child(Node.s32('day', statistics.today_plays))
con.add_child(Node.s32('cnt', statistics.total_plays))
con.add_child(Node.s32('last', statistics.last_play_timestamp))
con.add_child(Node.s32('now', Time.now()))
team = Node.void('team')

View File

@ -310,18 +310,6 @@ class ReflecBeatVolzza(ReflecBeatVolzzaBase):
pdata = Node.void('pdata')
root.add_child(pdata)
# Account time info
last_play_date = statistics.get_int_array('last_play_date', 3)
today_play_date = Time.todays_date()
if (
last_play_date[0] == today_play_date[0] and
last_play_date[1] == today_play_date[1] and
last_play_date[2] == today_play_date[2]
):
today_count = statistics.get_int('today_plays', 0)
else:
today_count = 0
# Previous account info
previous_version = self.previous_version()
if previous_version:
@ -333,11 +321,11 @@ class ReflecBeatVolzza(ReflecBeatVolzzaBase):
account = Node.void('account')
pdata.add_child(account)
account.add_child(Node.s32('usrid', profile.extid))
account.add_child(Node.s32('tpc', statistics.get_int('total_plays', 0)))
account.add_child(Node.s32('dpc', today_count))
account.add_child(Node.s32('tpc', statistics.total_plays))
account.add_child(Node.s32('dpc', statistics.today_plays))
account.add_child(Node.s32('crd', 1))
account.add_child(Node.s32('brd', 1))
account.add_child(Node.s32('tdc', statistics.get_int('total_days', 0)))
account.add_child(Node.s32('tdc', statistics.total_days))
account.add_child(Node.s32('intrvld', 0))
account.add_child(Node.s16('ver', 0))
account.add_child(Node.u64('pst', 0))

View File

@ -330,18 +330,6 @@ class ReflecBeatVolzza2(ReflecBeatVolzzaBase):
pdata = Node.void('pdata')
root.add_child(pdata)
# Account time info
last_play_date = statistics.get_int_array('last_play_date', 3)
today_play_date = Time.todays_date()
if (
last_play_date[0] == today_play_date[0] and
last_play_date[1] == today_play_date[1] and
last_play_date[2] == today_play_date[2]
):
today_count = statistics.get_int('today_plays', 0)
else:
today_count = 0
# Previous account info
previous_version = self.previous_version()
if previous_version:
@ -353,11 +341,11 @@ class ReflecBeatVolzza2(ReflecBeatVolzzaBase):
account = Node.void('account')
pdata.add_child(account)
account.add_child(Node.s32('usrid', profile.extid))
account.add_child(Node.s32('tpc', statistics.get_int('total_plays', 0)))
account.add_child(Node.s32('dpc', today_count))
account.add_child(Node.s32('tpc', statistics.total_plays))
account.add_child(Node.s32('dpc', statistics.today_plays))
account.add_child(Node.s32('crd', 1))
account.add_child(Node.s32('brd', 1))
account.add_child(Node.s32('tdc', statistics.get_int('total_days', 0)))
account.add_child(Node.s32('tdc', statistics.total_days))
account.add_child(Node.s32('intrvld', 0))
account.add_child(Node.s16('ver', 0))
account.add_child(Node.bool('succeed', succeeded))

View File

@ -3,7 +3,7 @@ import copy
from typing import Any, Dict, List
from bemani.backend.sdvx.gravitywars import SoundVoltexGravityWars
from bemani.common import ID, Time, Profile
from bemani.common import ID, Profile
from bemani.data import UserID
from bemani.protocol import Node
@ -3149,19 +3149,9 @@ class SoundVoltexGravityWarsSeason1(
# Play statistics
statistics = self.get_play_statistics(userid)
last_play_date = statistics.get_int_array('last_play_date', 3)
today_play_date = Time.todays_date()
if (
last_play_date[0] == today_play_date[0] and
last_play_date[1] == today_play_date[1] and
last_play_date[2] == today_play_date[2]
):
today_count = statistics.get_int('today_plays', 0)
else:
today_count = 0
game.add_child(Node.u32('play_count', statistics.get_int('total_plays', 0)))
game.add_child(Node.u32('daily_count', today_count))
game.add_child(Node.u32('play_chain', statistics.get_int('consecutive_days', 0)))
game.add_child(Node.u32('play_count', statistics.total_plays))
game.add_child(Node.u32('daily_count', statistics.today_plays))
game.add_child(Node.u32('play_chain', statistics.consecutive_days))
# Last played stuff
if 'last' in profile:

View File

@ -3,7 +3,7 @@ import copy
from typing import Any, Dict, List, Tuple
from bemani.backend.sdvx.gravitywars import SoundVoltexGravityWars
from bemani.common import ID, Time, Profile
from bemani.common import ID, Profile
from bemani.data import Score, UserID
from bemani.protocol import Node
@ -3970,19 +3970,9 @@ class SoundVoltexGravityWarsSeason2(
# Play statistics
statistics = self.get_play_statistics(userid)
last_play_date = statistics.get_int_array('last_play_date', 3)
today_play_date = Time.todays_date()
if (
last_play_date[0] == today_play_date[0] and
last_play_date[1] == today_play_date[1] and
last_play_date[2] == today_play_date[2]
):
today_count = statistics.get_int('today_plays', 0)
else:
today_count = 0
game.add_child(Node.u32('play_count', statistics.get_int('total_plays', 0)))
game.add_child(Node.u32('daily_count', today_count))
game.add_child(Node.u32('play_chain', statistics.get_int('consecutive_days', 0)))
game.add_child(Node.u32('play_count', statistics.total_plays))
game.add_child(Node.u32('daily_count', statistics.today_plays))
game.add_child(Node.u32('play_chain', statistics.consecutive_days))
# Last played stuff
if 'last' in profile:

View File

@ -5,7 +5,7 @@ from typing import Any, Dict, List, Optional, Tuple
from bemani.backend.ess import EventLogHandler
from bemani.backend.sdvx.base import SoundVoltexBase
from bemani.backend.sdvx.gravitywars import SoundVoltexGravityWars
from bemani.common import ID, Time, Profile, VersionConstants
from bemani.common import ID, Profile, VersionConstants
from bemani.data import Score, UserID
from bemani.protocol import Node
@ -3858,19 +3858,9 @@ class SoundVoltexHeavenlyHaven(
# Play statistics
statistics = self.get_play_statistics(userid)
last_play_date = statistics.get_int_array('last_play_date', 3)
today_play_date = Time.todays_date()
if (
last_play_date[0] == today_play_date[0] and
last_play_date[1] == today_play_date[1] and
last_play_date[2] == today_play_date[2]
):
today_count = statistics.get_int('today_plays', 0)
else:
today_count = 0
game.add_child(Node.u32('play_count', statistics.get_int('total_plays', 0)))
game.add_child(Node.u32('today_count', today_count))
game.add_child(Node.u32('play_chain', statistics.get_int('consecutive_days', 0)))
game.add_child(Node.u32('play_count', statistics.total_plays))
game.add_child(Node.u32('today_count', statistics.today_plays))
game.add_child(Node.u32('play_chain', statistics.consecutive_days))
# Also exists but we don't support:
# - day_count: Number of days where this user had at least one play.

View File

@ -5,7 +5,7 @@ from typing import Any, Dict, List, Optional
from bemani.backend.ess import EventLogHandler
from bemani.backend.sdvx.base import SoundVoltexBase
from bemani.backend.sdvx.booth import SoundVoltexBooth
from bemani.common import Time, Profile, VersionConstants, ID
from bemani.common import Profile, VersionConstants, ID
from bemani.data import UserID
from bemani.protocol import Node
@ -2304,19 +2304,9 @@ class SoundVoltexInfiniteInfection(
# Play statistics
statistics = self.get_play_statistics(userid)
last_play_date = statistics.get_int_array('last_play_date', 3)
today_play_date = Time.todays_date()
if (
last_play_date[0] == today_play_date[0] and
last_play_date[1] == today_play_date[1] and
last_play_date[2] == today_play_date[2]
):
today_count = statistics.get_int('today_plays', 0)
else:
today_count = 0
game.add_child(Node.u32('play_count', statistics.get_int('total_plays', 0)))
game.add_child(Node.u32('daily_count', today_count))
game.add_child(Node.u32('play_chain', statistics.get_int('consecutive_days', 0)))
game.add_child(Node.u32('play_count', statistics.total_plays))
game.add_child(Node.u32('daily_count', statistics.today_plays))
game.add_child(Node.u32('play_chain', statistics.consecutive_days))
# Last played stuff
if 'last' in profile:

View File

@ -1,5 +1,5 @@
from bemani.common.model import Model
from bemani.common.validateddict import ValidatedDict, Profile, intish
from bemani.common.validateddict import ValidatedDict, Profile, PlayStatistics, intish
from bemani.common.http import HTTP
from bemani.common.constants import APIConstants, GameConstants, VersionConstants, DBConstants, BroadcastConstants
from bemani.common.card import CardCipher, CardCipherException
@ -14,6 +14,7 @@ __all__ = [
"Model",
"ValidatedDict",
"Profile",
"PlayStatistics",
"HTTP",
"APIConstants",
"GameConstants",

View File

@ -454,3 +454,37 @@ class Profile(ValidatedDict):
self.version = version
self.refid = refid
self.extid = extid
class PlayStatistics(ValidatedDict):
"""
A special case of a ValidatedDict, a play statistics object is guaranteed
to also contain several values representing last play times, total play times,
and the like.
"""
def __init__(
self,
game: GameConstants,
total_plays: int,
today_plays: int,
total_days: int,
consecutive_days: int,
first_play_timestamp: int,
last_play_timestamp: int,
extra_values: Dict[str, Any] = {},
) -> None:
super().__init__(extra_values or {})
self.game = game
# How many actual profiles saves have we registered across all games in this series.
self.total_plays = total_plays
# How many actual profile saves have we registered today, so far.
self.today_plays = today_plays
# How many total days that we have registered at least one profile save.
self.total_days = total_days
# How many consecutive days in a row we registered at least one profile save.
self.consecutive_days = consecutive_days
# The timestamp of the very first play session, in seconds.
self.first_play_timestamp = first_play_timestamp
# The timestamp of the very last play session, in seconds.
self.last_play_timestamp = last_play_timestamp

View File

@ -86,7 +86,7 @@ class APIData(APIProviderInterface, BaseData):
cursor = self.execute(
sql,
{
'timestamp': int(Time.now()),
'timestamp': Time.now(),
'name': name,
'token': str(uuid.uuid4()),
},
@ -175,7 +175,7 @@ class APIData(APIProviderInterface, BaseData):
cursor = self.execute(
sql,
{
'timestamp': int(Time.now()),
'timestamp': Time.now(),
'uri': uri,
'token': token,
},

View File

@ -88,7 +88,7 @@ class NetworkData(BaseData):
The ID of the newly created entry.
"""
sql = "INSERT INTO news (timestamp, title, body) VALUES (:timestamp, :title, :body)"
cursor = self.execute(sql, {'timestamp': int(Time.now()), 'title': title, 'body': body})
cursor = self.execute(sql, {'timestamp': Time.now(), 'title': title, 'body': body})
return cursor.lastrowid
def get_news(self, newsid: int) -> Optional[News]:

View File

@ -0,0 +1,260 @@
# vim: set fileencoding=utf-8
import unittest
from freezegun import freeze_time
from typing import Dict, Any
from unittest.mock import Mock
from bemani.backend.base import Base
from bemani.common import GameConstants, Time, ValidatedDict
from bemani.data import UserID
# Make the normally-abstract base instantiable so we can test it.
class InstantiableBase(Base):
game = GameConstants.BISHI_BASHI
version = -1
name = "Test Class"
# Make an easier mock implementation of load/save stats.
def mock_stats(existing_value: Dict[str, Any]) -> Mock:
data = Mock()
data.local = Mock()
data.local.game = Mock()
data.local.game.get_settings = Mock(return_value=ValidatedDict(existing_value) if existing_value else None)
data.local.game.put_settings = Mock()
return data
def saved_stats(mock: Mock) -> ValidatedDict:
return ValidatedDict(mock.local.game.put_settings.call_args.args[2])
class TestPlayStats(unittest.TestCase):
def test_get_brand_new_profile(self) -> None:
with freeze_time('2021-08-24'):
stats = None
data = mock_stats(stats)
base = InstantiableBase(data, Mock(), Mock())
settings = base.get_play_statistics(UserID(1337))
self.assertEqual(settings.game, GameConstants.BISHI_BASHI)
self.assertEqual(settings.total_plays, 1)
self.assertEqual(settings.today_plays, 1)
self.assertEqual(settings.total_days, 1)
self.assertEqual(settings.consecutive_days, 1)
self.assertEqual(settings.first_play_timestamp, Time.now())
self.assertEqual(settings.last_play_timestamp, Time.now())
def test_put_brand_new_profile(self) -> None:
with freeze_time('2021-08-24'):
stats = None
data = mock_stats(stats)
base = InstantiableBase(data, Mock(), Mock())
settings = base.get_play_statistics(UserID(1337))
base.update_play_statistics(UserID(1337), settings)
new_settings = saved_stats(data)
self.assertEqual(new_settings.get_int('total_plays'), 1)
self.assertEqual(new_settings.get_int('today_plays'), 1)
self.assertEqual(new_settings.get_int('total_days'), 1)
self.assertEqual(new_settings.get_int('consecutive_days'), 1)
self.assertEqual(new_settings.get_int('first_play_timestamp'), Time.now())
self.assertEqual(new_settings.get_int('last_play_timestamp'), Time.now())
self.assertEqual(new_settings.get_int_array('last_play_date', 3), Time.todays_date())
def test_get_played_today(self) -> None:
with freeze_time('2021-08-24'):
play_date = Time.todays_date()
stats = {
'total_plays': 1234,
'today_plays': 420,
'total_days': 10,
'first_play_timestamp': 1234567890,
'last_play_timestamp': 9876543210,
'consecutive_days': 69,
'last_play_date': [play_date[0], play_date[1], play_date[2]],
}
data = mock_stats(stats)
base = InstantiableBase(data, Mock(), Mock())
settings = base.get_play_statistics(UserID(1337))
self.assertEqual(settings.game, GameConstants.BISHI_BASHI)
self.assertEqual(settings.total_plays, 1235)
self.assertEqual(settings.today_plays, 421)
self.assertEqual(settings.total_days, 10)
self.assertEqual(settings.consecutive_days, 69)
self.assertEqual(settings.first_play_timestamp, 1234567890)
self.assertEqual(settings.last_play_timestamp, 9876543210)
def test_put_played_today(self) -> None:
with freeze_time('2021-08-24'):
play_date = Time.todays_date()
stats = {
'total_plays': 1234,
'today_plays': 420,
'total_days': 10,
'first_play_timestamp': 1234567890,
'last_play_timestamp': 1234567890,
'consecutive_days': 69,
'last_play_date': [play_date[0], play_date[1], play_date[2]],
}
data = mock_stats(stats)
base = InstantiableBase(data, Mock(), Mock())
settings = base.get_play_statistics(UserID(1337))
base.update_play_statistics(UserID(1337), settings)
new_settings = saved_stats(data)
self.assertEqual(new_settings.get_int('total_plays'), 1235)
self.assertEqual(new_settings.get_int('today_plays'), 421)
self.assertEqual(new_settings.get_int('total_days'), 10)
self.assertEqual(new_settings.get_int('consecutive_days'), 69)
self.assertEqual(new_settings.get_int('first_play_timestamp'), 1234567890)
self.assertEqual(new_settings.get_int('last_play_timestamp'), Time.now())
self.assertEqual(new_settings.get_int_array('last_play_date', 3), Time.todays_date())
def test_get_played_yesterday(self) -> None:
with freeze_time('2021-08-24'):
play_date = Time.yesterdays_date()
stats = {
'total_plays': 1234,
'today_plays': 420,
'total_days': 10,
'first_play_timestamp': 1234567890,
'last_play_timestamp': 9876543210,
'consecutive_days': 69,
'last_play_date': [play_date[0], play_date[1], play_date[2]],
}
data = mock_stats(stats)
base = InstantiableBase(data, Mock(), Mock())
settings = base.get_play_statistics(UserID(1337))
self.assertEqual(settings.game, GameConstants.BISHI_BASHI)
self.assertEqual(settings.total_plays, 1235)
self.assertEqual(settings.today_plays, 1)
self.assertEqual(settings.total_days, 11)
self.assertEqual(settings.consecutive_days, 70)
self.assertEqual(settings.first_play_timestamp, 1234567890)
self.assertEqual(settings.last_play_timestamp, 9876543210)
def test_put_played_yesterday(self) -> None:
with freeze_time('2021-08-24'):
play_date = Time.yesterdays_date()
stats = {
'total_plays': 1234,
'today_plays': 420,
'total_days': 10,
'first_play_timestamp': 1234567890,
'last_play_timestamp': 1234567890,
'consecutive_days': 69,
'last_play_date': [play_date[0], play_date[1], play_date[2]],
}
data = mock_stats(stats)
base = InstantiableBase(data, Mock(), Mock())
settings = base.get_play_statistics(UserID(1337))
base.update_play_statistics(UserID(1337), settings)
new_settings = saved_stats(data)
self.assertEqual(new_settings.get_int('total_plays'), 1235)
self.assertEqual(new_settings.get_int('today_plays'), 1)
self.assertEqual(new_settings.get_int('total_days'), 11)
self.assertEqual(new_settings.get_int('consecutive_days'), 70)
self.assertEqual(new_settings.get_int('first_play_timestamp'), 1234567890)
self.assertEqual(new_settings.get_int('last_play_timestamp'), Time.now())
self.assertEqual(new_settings.get_int_array('last_play_date', 3), Time.todays_date())
def test_get_played_awhile_ago(self) -> None:
with freeze_time('2021-08-24'):
stats = {
'total_plays': 1234,
'today_plays': 420,
'total_days': 10,
'first_play_timestamp': 1234567890,
'last_play_timestamp': 9876543210,
'consecutive_days': 69,
'last_play_date': [2010, 4, 20],
}
data = mock_stats(stats)
base = InstantiableBase(data, Mock(), Mock())
settings = base.get_play_statistics(UserID(1337))
self.assertEqual(settings.game, GameConstants.BISHI_BASHI)
self.assertEqual(settings.total_plays, 1235)
self.assertEqual(settings.today_plays, 1)
self.assertEqual(settings.total_days, 11)
self.assertEqual(settings.consecutive_days, 1)
self.assertEqual(settings.first_play_timestamp, 1234567890)
self.assertEqual(settings.last_play_timestamp, 9876543210)
def test_put_played_awhile_ago(self) -> None:
with freeze_time('2021-08-24'):
stats = {
'total_plays': 1234,
'today_plays': 420,
'total_days': 10,
'first_play_timestamp': 1234567890,
'last_play_timestamp': 1234567890,
'consecutive_days': 69,
'last_play_date': [2010, 4, 20],
}
data = mock_stats(stats)
base = InstantiableBase(data, Mock(), Mock())
settings = base.get_play_statistics(UserID(1337))
base.update_play_statistics(UserID(1337), settings)
new_settings = saved_stats(data)
self.assertEqual(new_settings.get_int('total_plays'), 1235)
self.assertEqual(new_settings.get_int('today_plays'), 1)
self.assertEqual(new_settings.get_int('total_days'), 11)
self.assertEqual(new_settings.get_int('consecutive_days'), 1)
self.assertEqual(new_settings.get_int('first_play_timestamp'), 1234567890)
self.assertEqual(new_settings.get_int('last_play_timestamp'), Time.now())
self.assertEqual(new_settings.get_int_array('last_play_date', 3), Time.todays_date())
def test_get_extra_settings(self) -> None:
with freeze_time('2021-08-24'):
stats = {
'total_plays': 1234,
'key': 'value',
'int': 1337,
}
data = mock_stats(stats)
base = InstantiableBase(data, Mock(), Mock())
settings = base.get_play_statistics(UserID(1337))
self.assertEqual(settings.get_int('int'), 1337)
self.assertEqual(settings.get_str('key'), 'value')
self.assertEqual(settings.get_int('total_plays'), 0)
def test_put_extra_settings(self) -> None:
with freeze_time('2021-08-24'):
stats = {
'total_plays': 1234,
'key': 'value',
'int': 1337,
}
data = mock_stats(stats)
base = InstantiableBase(data, Mock(), Mock())
settings = base.get_play_statistics(UserID(1337))
settings.replace_int('int', 420)
settings.replace_int('int2', 69)
settings.replace_int('total_plays', 37)
base.update_play_statistics(UserID(1337), settings)
new_settings = saved_stats(data)
self.assertEqual(new_settings.get_int('int'), 420)
self.assertEqual(new_settings.get_str('key'), 'value')
self.assertEqual(new_settings.get_int('int2'), 69)
self.assertEqual(new_settings.get_int('total_plays'), 1235)