mirror of
https://github.com/283375/arcaea-offline.git
synced 2025-04-18 13:50:16 +00:00
Compare commits
3 Commits
2a2a063a3c
...
dbb5d680af
Author | SHA1 | Date | |
---|---|---|---|
dbb5d680af | |||
3e6cc8d7e7 | |||
10b332e911 |
3
.github/workflows/main.yml
vendored
3
.github/workflows/main.yml
vendored
@ -14,6 +14,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ['3.8', '3.9', '3.10', '3.11', '3.12']
|
python-version: ['3.8', '3.9', '3.10', '3.11', '3.12']
|
||||||
|
fail-fast: false
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@ -24,7 +25,7 @@ jobs:
|
|||||||
- name: Install dev dependencies
|
- name: Install dev dependencies
|
||||||
run: 'pip install .[dev]'
|
run: 'pip install .[dev]'
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: 'pytest -v'
|
run: 'python -m pytest -v'
|
||||||
|
|
||||||
ruff:
|
ruff:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
@ -9,10 +9,7 @@ authors = [{ name = "283375", email = "log_283375@163.com" }]
|
|||||||
description = "Manage your local Arcaea score database."
|
description = "Manage your local Arcaea score database."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
dependencies = [
|
dependencies = ["SQLAlchemy==2.0.20", "SQLAlchemy-Utils==0.41.1"]
|
||||||
"SQLAlchemy==2.0.20",
|
|
||||||
"SQLAlchemy-Utils==0.41.1",
|
|
||||||
]
|
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Development Status :: 3 - Alpha",
|
"Development Status :: 3 - Alpha",
|
||||||
"Programming Language :: Python :: 3",
|
"Programming Language :: Python :: 3",
|
||||||
@ -49,3 +46,8 @@ select = [
|
|||||||
ignore = [
|
ignore = [
|
||||||
"E501", # line-too-long
|
"E501", # line-too-long
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[tool.ruff.lint.per-file-ignores]
|
||||||
|
"tests/*" = [
|
||||||
|
"PLR2004", # magic-value-comparison
|
||||||
|
]
|
||||||
|
@ -6,11 +6,8 @@ from sqlalchemy import Engine, func, inspect, select
|
|||||||
from sqlalchemy.orm import DeclarativeBase, InstrumentedAttribute, sessionmaker
|
from sqlalchemy.orm import DeclarativeBase, InstrumentedAttribute, sessionmaker
|
||||||
|
|
||||||
from arcaea_offline.external.arcsong.arcsong_json import ArcSongJsonBuilder
|
from arcaea_offline.external.arcsong.arcsong_json import ArcSongJsonBuilder
|
||||||
from arcaea_offline.external.exports import (
|
from arcaea_offline.external.exports import exporters
|
||||||
ArcaeaOfflineDEFV2_Score,
|
from arcaea_offline.external.exports.types import ArcaeaOfflineDEFV2_Score, ScoreExport
|
||||||
ScoreExport,
|
|
||||||
exporters,
|
|
||||||
)
|
|
||||||
from arcaea_offline.singleton import Singleton
|
from arcaea_offline.singleton import Singleton
|
||||||
|
|
||||||
from .models.v4.config import ConfigBase, Property
|
from .models.v4.config import ConfigBase, Property
|
||||||
|
118
src/arcaea_offline/external/importers/arcaea/st3.py
vendored
Normal file
118
src/arcaea_offline/external/importers/arcaea/st3.py
vendored
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
"""
|
||||||
|
Game database play results importer
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import sqlite3
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from typing import List, overload
|
||||||
|
|
||||||
|
from arcaea_offline.constants.enums import (
|
||||||
|
ArcaeaPlayResultClearType,
|
||||||
|
ArcaeaPlayResultModifier,
|
||||||
|
ArcaeaRatingClass,
|
||||||
|
)
|
||||||
|
from arcaea_offline.database.models.v5 import PlayResult
|
||||||
|
|
||||||
|
from .common import fix_timestamp
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class St3Parser:
|
||||||
|
@classmethod
|
||||||
|
@overload
|
||||||
|
def parse(cls, db: sqlite3.Connection) -> List[PlayResult]: ...
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@overload
|
||||||
|
def parse(cls, db: sqlite3.Cursor) -> List[PlayResult]: ...
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def parse(cls, db) -> List[PlayResult]:
|
||||||
|
if isinstance(db, sqlite3.Connection):
|
||||||
|
return cls.parse(db.cursor())
|
||||||
|
|
||||||
|
if not isinstance(db, sqlite3.Cursor):
|
||||||
|
raise TypeError(
|
||||||
|
"Unknown overload of `db`. Expected `sqlite3.Connection` or `sqlite3.Cursor`."
|
||||||
|
)
|
||||||
|
|
||||||
|
entities = []
|
||||||
|
query_results = db.execute("""
|
||||||
|
SELECT s.id AS _id, s.songId, s.songDifficulty AS ratingClass, s.score,
|
||||||
|
s.perfectCount AS pure, s.nearCount AS far, s.missCount AS lost,
|
||||||
|
s.`date`, s.modifier, ct.clearType
|
||||||
|
FROM scores s JOIN cleartypes ct
|
||||||
|
ON s.songId = ct.songId AND s.songDifficulty = ct.songDifficulty""")
|
||||||
|
# maybe `s.id = ct.id`?
|
||||||
|
|
||||||
|
now = datetime.now(tz=timezone.utc)
|
||||||
|
import_comment = (
|
||||||
|
f"Imported from st3 at {now.astimezone().isoformat(timespec='seconds')}"
|
||||||
|
)
|
||||||
|
for result in query_results:
|
||||||
|
(
|
||||||
|
_id,
|
||||||
|
song_id,
|
||||||
|
rating_class,
|
||||||
|
score,
|
||||||
|
pure,
|
||||||
|
far,
|
||||||
|
lost,
|
||||||
|
date,
|
||||||
|
modifier,
|
||||||
|
clear_type,
|
||||||
|
) = result
|
||||||
|
|
||||||
|
try:
|
||||||
|
rating_class_enum = ArcaeaRatingClass(rating_class)
|
||||||
|
except ValueError:
|
||||||
|
logger.warning(
|
||||||
|
"Unknown rating class [%r] at entry id %d, skipping!",
|
||||||
|
rating_class,
|
||||||
|
_id,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
clear_type_enum = ArcaeaPlayResultClearType(clear_type)
|
||||||
|
except ValueError:
|
||||||
|
logger.warning(
|
||||||
|
"Unknown clear type [%r] at entry id %d, falling back to `None`!",
|
||||||
|
clear_type,
|
||||||
|
_id,
|
||||||
|
)
|
||||||
|
clear_type_enum = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
modifier_enum = ArcaeaPlayResultModifier(modifier)
|
||||||
|
except ValueError:
|
||||||
|
logger.warning(
|
||||||
|
"Unknown modifier [%r] at entry id %d, falling back to `None`!",
|
||||||
|
modifier,
|
||||||
|
_id,
|
||||||
|
)
|
||||||
|
modifier_enum = None
|
||||||
|
|
||||||
|
if date := fix_timestamp(date):
|
||||||
|
date = datetime.fromtimestamp(date).astimezone()
|
||||||
|
else:
|
||||||
|
date = None
|
||||||
|
|
||||||
|
entities.append(
|
||||||
|
PlayResult(
|
||||||
|
song_id=song_id,
|
||||||
|
rating_class=rating_class_enum,
|
||||||
|
score=score,
|
||||||
|
pure=pure,
|
||||||
|
far=far,
|
||||||
|
lost=lost,
|
||||||
|
date=date,
|
||||||
|
modifier=modifier_enum,
|
||||||
|
clear_type=clear_type_enum,
|
||||||
|
comment=import_comment,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return entities
|
56
tests/external/importers/arcaea/test_st3.py
vendored
Normal file
56
tests/external/importers/arcaea/test_st3.py
vendored
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import sqlite3
|
||||||
|
from datetime import datetime
|
||||||
|
from importlib.resources import files
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from arcaea_offline.constants.enums.arcaea import (
|
||||||
|
ArcaeaPlayResultClearType,
|
||||||
|
ArcaeaPlayResultModifier,
|
||||||
|
ArcaeaRatingClass,
|
||||||
|
)
|
||||||
|
from arcaea_offline.external.importers.arcaea.st3 import St3Parser
|
||||||
|
|
||||||
|
import tests.resources
|
||||||
|
|
||||||
|
|
||||||
|
class TestSt3Parser:
|
||||||
|
DB_PATH = files(tests.resources).joinpath("st3-test.db")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def play_results(self):
|
||||||
|
conn = sqlite3.connect(str(self.DB_PATH))
|
||||||
|
return St3Parser.parse(conn)
|
||||||
|
|
||||||
|
def test_basic(self):
|
||||||
|
play_results = self.play_results
|
||||||
|
|
||||||
|
assert len(play_results) == 4
|
||||||
|
|
||||||
|
test1 = next(filter(lambda x: x.song_id == "test1", play_results))
|
||||||
|
assert test1.rating_class is ArcaeaRatingClass.FUTURE
|
||||||
|
assert test1.score == 9441167
|
||||||
|
assert test1.pure == 895
|
||||||
|
assert test1.far == 32
|
||||||
|
assert test1.lost == 22
|
||||||
|
assert test1.date == datetime.fromtimestamp(1722100000).astimezone()
|
||||||
|
assert test1.clear_type is ArcaeaPlayResultClearType.TRACK_LOST
|
||||||
|
assert test1.modifier is ArcaeaPlayResultModifier.HARD
|
||||||
|
|
||||||
|
def test_corrupt_handling(self):
|
||||||
|
play_results = self.play_results
|
||||||
|
|
||||||
|
corrupt1 = filter(lambda x: x.song_id == "corrupt1", play_results)
|
||||||
|
# `rating_class` out of range, so this should be ignored during parsing,
|
||||||
|
# thus does not present in the result.
|
||||||
|
assert len(list(corrupt1)) == 0
|
||||||
|
|
||||||
|
corrupt2 = next(filter(lambda x: x.song_id == "corrupt2", play_results))
|
||||||
|
assert corrupt2.clear_type is None
|
||||||
|
assert corrupt2.modifier is None
|
||||||
|
|
||||||
|
date1 = next(filter(lambda x: x.song_id == "date1", play_results))
|
||||||
|
assert date1.date is None
|
||||||
|
|
||||||
|
def test_invalid_input(self):
|
||||||
|
pytest.raises(TypeError, St3Parser.parse, "abcdefghijklmn")
|
||||||
|
pytest.raises(TypeError, St3Parser.parse, 123456)
|
0
tests/resources/__init__.py
Normal file
0
tests/resources/__init__.py
Normal file
BIN
tests/resources/st3-test.db
Normal file
BIN
tests/resources/st3-test.db
Normal file
Binary file not shown.
117
tests/resources/st3-test.json
Normal file
117
tests/resources/st3-test.json
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
{
|
||||||
|
"_comment": "A quick preview of the data in st3-test.db",
|
||||||
|
"scores": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"version": 1,
|
||||||
|
"score": 9441167,
|
||||||
|
"shinyPerfectCount": 753,
|
||||||
|
"perfectCount": 895,
|
||||||
|
"nearCount": 32,
|
||||||
|
"missCount": 22,
|
||||||
|
"date": 1722100000,
|
||||||
|
"songId": "test1",
|
||||||
|
"songDifficulty": 2,
|
||||||
|
"modifier": 2,
|
||||||
|
"health": 0,
|
||||||
|
"ct": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"version": 1,
|
||||||
|
"score": 9752087,
|
||||||
|
"shinyPerfectCount": 914,
|
||||||
|
"perfectCount": 1024,
|
||||||
|
"nearCount": 29,
|
||||||
|
"missCount": 12,
|
||||||
|
"date": 1722200000,
|
||||||
|
"songId": "test2",
|
||||||
|
"songDifficulty": 2,
|
||||||
|
"modifier": 0,
|
||||||
|
"health": 100,
|
||||||
|
"ct": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"version": 1,
|
||||||
|
"score": 9750000,
|
||||||
|
"shinyPerfectCount": 900,
|
||||||
|
"perfectCount": 1000,
|
||||||
|
"nearCount": 20,
|
||||||
|
"missCount": 10,
|
||||||
|
"date": 1722200000,
|
||||||
|
"songId": "corrupt1",
|
||||||
|
"songDifficulty": 5,
|
||||||
|
"modifier": 0,
|
||||||
|
"health": 0,
|
||||||
|
"ct": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"version": 1,
|
||||||
|
"score": 9750000,
|
||||||
|
"shinyPerfectCount": 900,
|
||||||
|
"perfectCount": 1000,
|
||||||
|
"nearCount": 20,
|
||||||
|
"missCount": 10,
|
||||||
|
"date": 1722200000,
|
||||||
|
"songId": "corrupt2",
|
||||||
|
"songDifficulty": 2,
|
||||||
|
"modifier": 9,
|
||||||
|
"health": 0,
|
||||||
|
"ct": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"version": 1,
|
||||||
|
"score": 9750000,
|
||||||
|
"shinyPerfectCount": 900,
|
||||||
|
"perfectCount": 1000,
|
||||||
|
"nearCount": 20,
|
||||||
|
"missCount": 10,
|
||||||
|
"date": 1,
|
||||||
|
"songId": "date1",
|
||||||
|
"songDifficulty": 2,
|
||||||
|
"modifier": 0,
|
||||||
|
"health": 0,
|
||||||
|
"ct": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cleartypes": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"songId": "test1",
|
||||||
|
"songDifficulty": 2,
|
||||||
|
"clearType": 0,
|
||||||
|
"ct": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"songId": "test2",
|
||||||
|
"songDifficulty": 2,
|
||||||
|
"clearType": 1,
|
||||||
|
"ct": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"songId": "corrupt1",
|
||||||
|
"songDifficulty": 5,
|
||||||
|
"clearType": 0,
|
||||||
|
"ct": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"songId": "corrupt2",
|
||||||
|
"songDifficulty": 2,
|
||||||
|
"clearType": 7,
|
||||||
|
"ct": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"songId": "date1",
|
||||||
|
"songDifficulty": 2,
|
||||||
|
"clearType": 1,
|
||||||
|
"ct": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user