Splatlogger 1.6.2__tar.gz → 1.6.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- splatlogger-1.6.3/MANIFEST.in +1 -0
- {splatlogger-1.6.2 → splatlogger-1.6.3}/PKG-INFO +19 -12
- {splatlogger-1.6.2 → splatlogger-1.6.3}/README.md +17 -10
- {splatlogger-1.6.2 → splatlogger-1.6.3}/pyproject.toml +2 -2
- {splatlogger-1.6.2 → splatlogger-1.6.3}/src/Splatlogger.egg-info/PKG-INFO +19 -12
- {splatlogger-1.6.2 → splatlogger-1.6.3}/src/Splatlogger.egg-info/SOURCES.txt +6 -2
- splatlogger-1.6.3/src/splatlogger/__main__.py +404 -0
- {splatlogger-1.6.2 → splatlogger-1.6.3}/src/splatlogger/cemu_gecko.py +19 -9
- splatlogger-1.6.3/src/splatlogger/data/api.json +4 -0
- splatlogger-1.6.3/src/splatlogger/data/names.json +725 -0
- splatlogger-1.6.3/src/splatlogger/enums.py +33 -0
- splatlogger-1.6.3/src/splatlogger/match_logger.py +210 -0
- splatlogger-1.6.3/src/splatlogger/tcpgecko.py +159 -0
- splatlogger-1.6.3/src/splatlogger/tcpgecko_aroma.py +109 -0
- splatlogger-1.6.3/src/splatlogger/tuples.py +46 -0
- splatlogger-1.6.2/src/splatlogger/__main__.py +0 -337
- splatlogger-1.6.2/src/splatlogger/data.py +0 -764
- splatlogger-1.6.2/src/splatlogger/match_logger.py +0 -185
- splatlogger-1.6.2/src/splatlogger/tcpgecko.py +0 -162
- splatlogger-1.6.2/src/splatlogger/tcpgecko_aroma.py +0 -116
- {splatlogger-1.6.2 → splatlogger-1.6.3}/setup.cfg +0 -0
- {splatlogger-1.6.2 → splatlogger-1.6.3}/src/Splatlogger.egg-info/dependency_links.txt +0 -0
- {splatlogger-1.6.2 → splatlogger-1.6.3}/src/Splatlogger.egg-info/entry_points.txt +0 -0
- {splatlogger-1.6.2 → splatlogger-1.6.3}/src/Splatlogger.egg-info/requires.txt +0 -0
- {splatlogger-1.6.2 → splatlogger-1.6.3}/src/Splatlogger.egg-info/top_level.txt +0 -0
- {splatlogger-1.6.2 → splatlogger-1.6.3}/src/splatlogger/__init__.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
include src/splatlogger/data/*.json
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: Splatlogger
|
|
3
|
-
Version: 1.6.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 1.6.3
|
|
4
|
+
Summary: PID (Principal ID)/PNID grabber and match logger for Splatoon
|
|
5
5
|
Author-email: Shadow Doggo <shadowdoggo@protonmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Source Code, https://codeberg.org/ShadowDoggo/Splatlogger
|
|
@@ -15,7 +15,7 @@ Requires-Dist: psutil
|
|
|
15
15
|
Requires-Dist: pymemoryeditor
|
|
16
16
|
|
|
17
17
|
# Splatlogger
|
|
18
|
-
A CLI PID (Principal ID)/PNID grabber and match logger for Splatoon, written in Python.
|
|
18
|
+
A CLI PID (Principal ID)/PNID grabber and match logger for Splatoon (Wii U), written in Python.
|
|
19
19
|
|
|
20
20
|
## Prerequisites
|
|
21
21
|
To install the package, you'll need to have Python 3.11 or newer installed on your system.
|
|
@@ -40,6 +40,7 @@ To install the package from PyPI, run:
|
|
|
40
40
|
```
|
|
41
41
|
pip install Splatlogger
|
|
42
42
|
```
|
|
43
|
+
(To upgrade from an older version, use `pip install --upgrade`)
|
|
43
44
|
|
|
44
45
|
Or download the latest release from Codeberg and run:
|
|
45
46
|
```
|
|
@@ -72,7 +73,13 @@ path/to/Splatlogger-1.x.exe -ip IP [options]
|
|
|
72
73
|
```
|
|
73
74
|
|
|
74
75
|
Options:
|
|
75
|
-
- `-
|
|
76
|
+
- `-aroma` - Switch to Aroma mode. Enable if using the TCPGecko Aroma plugin.
|
|
77
|
+
|
|
78
|
+
- `-cemu [PID]` - Switch to Cemu mode. The `-ip` argument is not required.
|
|
79
|
+
Note: If you have ptrace protection enabled, you'll need to run Python as root. It's best to run the module from source in that case.
|
|
80
|
+
- `PID` - PID of the Cemu process (optional).
|
|
81
|
+
|
|
82
|
+
- `-log-level option` - Set how much data should be logged.
|
|
76
83
|
- `none` - Don't create a log file.
|
|
77
84
|
- `standard` - Log only basic player information, the same as what's printed to the console (default).
|
|
78
85
|
- `extended` - Log all player information and additional match information.
|
|
@@ -82,20 +89,18 @@ Options:
|
|
|
82
89
|
- `all` - Save a log of all matches you play (default).
|
|
83
90
|
- `latest` - Save a log of only the latest match.
|
|
84
91
|
|
|
85
|
-
- `-aroma` - Switch to Aroma mode. Enable if using the TCPGecko Aroma plugin.
|
|
86
|
-
|
|
87
|
-
- `-cemu [PID]` - Switch to Cemu mode. The `-ip` argument is not required and will be ignored.
|
|
88
|
-
*Note: If you have ptrace protection enabled, you'll need to run python as root. It's best to run the module from source in that case.*
|
|
89
|
-
- `PID` - PID of the Cemu process (optional).
|
|
90
|
-
|
|
91
92
|
- `-stack Address (in hex)` - (Cemu only, optional) Beginning address of the stack space for Default Core 1 (Debug > View PPC threads).
|
|
92
93
|
Only required for logging stats. If the default values don't work, you'll need to specify this manually.
|
|
93
94
|
|
|
94
95
|
<img src="stack.png" width="600" alt="(example)">
|
|
95
96
|
|
|
97
|
+
- `-spfn` - Fetch SFID instead of PNID. Enable if playing on SPFN.
|
|
98
|
+
|
|
99
|
+
- `-no-pnid` - Disable fetching PNIDs.
|
|
100
|
+
|
|
96
101
|
- `-silent` - Disable printing logs to the console.
|
|
97
102
|
|
|
98
|
-
Logs are saved in `(User)/Documents/Splatlogger/logs
|
|
103
|
+
Logs are saved in `(User)/Documents/Splatlogger/logs/`.
|
|
99
104
|
|
|
100
105
|
To always run with the same IP and options without having to type them in manually, create an `args.txt` file in
|
|
101
106
|
`(User)/Documents/Splatlogger/` and put the arguments following `splatlogger` in there.
|
|
@@ -104,6 +109,8 @@ Afterward any arguments passed through the command line will be ignored.
|
|
|
104
109
|
Only one program can be connected to TCPGecko at a time. If you have something else connected, disconnect it beforehand.
|
|
105
110
|
|
|
106
111
|
## Credits
|
|
107
|
-
- [pyGecko](https://github.com/wiiudev/pyGecko) authors for
|
|
112
|
+
- [pyGecko](https://github.com/wiiudev/pyGecko) authors for the reference implementation of a Python TCPGecko library.
|
|
108
113
|
|
|
109
114
|
- Everyone who contributed to [PNIDGrab](https://github.com/JerrySM64/PNIDGrab) and other similar PID grabbers as those were used as a reference.
|
|
115
|
+
|
|
116
|
+
- Special thanks to Pirlo for helping me with certain stuff.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Splatlogger
|
|
2
|
-
A CLI PID (Principal ID)/PNID grabber and match logger for Splatoon, written in Python.
|
|
2
|
+
A CLI PID (Principal ID)/PNID grabber and match logger for Splatoon (Wii U), written in Python.
|
|
3
3
|
|
|
4
4
|
## Prerequisites
|
|
5
5
|
To install the package, you'll need to have Python 3.11 or newer installed on your system.
|
|
@@ -24,6 +24,7 @@ To install the package from PyPI, run:
|
|
|
24
24
|
```
|
|
25
25
|
pip install Splatlogger
|
|
26
26
|
```
|
|
27
|
+
(To upgrade from an older version, use `pip install --upgrade`)
|
|
27
28
|
|
|
28
29
|
Or download the latest release from Codeberg and run:
|
|
29
30
|
```
|
|
@@ -56,7 +57,13 @@ path/to/Splatlogger-1.x.exe -ip IP [options]
|
|
|
56
57
|
```
|
|
57
58
|
|
|
58
59
|
Options:
|
|
59
|
-
- `-
|
|
60
|
+
- `-aroma` - Switch to Aroma mode. Enable if using the TCPGecko Aroma plugin.
|
|
61
|
+
|
|
62
|
+
- `-cemu [PID]` - Switch to Cemu mode. The `-ip` argument is not required.
|
|
63
|
+
Note: If you have ptrace protection enabled, you'll need to run Python as root. It's best to run the module from source in that case.
|
|
64
|
+
- `PID` - PID of the Cemu process (optional).
|
|
65
|
+
|
|
66
|
+
- `-log-level option` - Set how much data should be logged.
|
|
60
67
|
- `none` - Don't create a log file.
|
|
61
68
|
- `standard` - Log only basic player information, the same as what's printed to the console (default).
|
|
62
69
|
- `extended` - Log all player information and additional match information.
|
|
@@ -66,20 +73,18 @@ Options:
|
|
|
66
73
|
- `all` - Save a log of all matches you play (default).
|
|
67
74
|
- `latest` - Save a log of only the latest match.
|
|
68
75
|
|
|
69
|
-
- `-aroma` - Switch to Aroma mode. Enable if using the TCPGecko Aroma plugin.
|
|
70
|
-
|
|
71
|
-
- `-cemu [PID]` - Switch to Cemu mode. The `-ip` argument is not required and will be ignored.
|
|
72
|
-
*Note: If you have ptrace protection enabled, you'll need to run python as root. It's best to run the module from source in that case.*
|
|
73
|
-
- `PID` - PID of the Cemu process (optional).
|
|
74
|
-
|
|
75
76
|
- `-stack Address (in hex)` - (Cemu only, optional) Beginning address of the stack space for Default Core 1 (Debug > View PPC threads).
|
|
76
77
|
Only required for logging stats. If the default values don't work, you'll need to specify this manually.
|
|
77
78
|
|
|
78
79
|
<img src="stack.png" width="600" alt="(example)">
|
|
79
80
|
|
|
81
|
+
- `-spfn` - Fetch SFID instead of PNID. Enable if playing on SPFN.
|
|
82
|
+
|
|
83
|
+
- `-no-pnid` - Disable fetching PNIDs.
|
|
84
|
+
|
|
80
85
|
- `-silent` - Disable printing logs to the console.
|
|
81
86
|
|
|
82
|
-
Logs are saved in `(User)/Documents/Splatlogger/logs
|
|
87
|
+
Logs are saved in `(User)/Documents/Splatlogger/logs/`.
|
|
83
88
|
|
|
84
89
|
To always run with the same IP and options without having to type them in manually, create an `args.txt` file in
|
|
85
90
|
`(User)/Documents/Splatlogger/` and put the arguments following `splatlogger` in there.
|
|
@@ -88,6 +93,8 @@ Afterward any arguments passed through the command line will be ignored.
|
|
|
88
93
|
Only one program can be connected to TCPGecko at a time. If you have something else connected, disconnect it beforehand.
|
|
89
94
|
|
|
90
95
|
## Credits
|
|
91
|
-
- [pyGecko](https://github.com/wiiudev/pyGecko) authors for
|
|
96
|
+
- [pyGecko](https://github.com/wiiudev/pyGecko) authors for the reference implementation of a Python TCPGecko library.
|
|
92
97
|
|
|
93
98
|
- Everyone who contributed to [PNIDGrab](https://github.com/JerrySM64/PNIDGrab) and other similar PID grabbers as those were used as a reference.
|
|
99
|
+
|
|
100
|
+
- Special thanks to Pirlo for helping me with certain stuff.
|
|
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "Splatlogger"
|
|
7
|
-
version = "1.6.
|
|
7
|
+
version = "1.6.3"
|
|
8
8
|
authors = [
|
|
9
9
|
{ name="Shadow Doggo", email="shadowdoggo@protonmail.com" }
|
|
10
10
|
]
|
|
11
|
-
description = "
|
|
11
|
+
description = "PID (Principal ID)/PNID grabber and match logger for Splatoon"
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
requires-python = ">=3.11"
|
|
14
14
|
dependencies = [
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: Splatlogger
|
|
3
|
-
Version: 1.6.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 1.6.3
|
|
4
|
+
Summary: PID (Principal ID)/PNID grabber and match logger for Splatoon
|
|
5
5
|
Author-email: Shadow Doggo <shadowdoggo@protonmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Source Code, https://codeberg.org/ShadowDoggo/Splatlogger
|
|
@@ -15,7 +15,7 @@ Requires-Dist: psutil
|
|
|
15
15
|
Requires-Dist: pymemoryeditor
|
|
16
16
|
|
|
17
17
|
# Splatlogger
|
|
18
|
-
A CLI PID (Principal ID)/PNID grabber and match logger for Splatoon, written in Python.
|
|
18
|
+
A CLI PID (Principal ID)/PNID grabber and match logger for Splatoon (Wii U), written in Python.
|
|
19
19
|
|
|
20
20
|
## Prerequisites
|
|
21
21
|
To install the package, you'll need to have Python 3.11 or newer installed on your system.
|
|
@@ -40,6 +40,7 @@ To install the package from PyPI, run:
|
|
|
40
40
|
```
|
|
41
41
|
pip install Splatlogger
|
|
42
42
|
```
|
|
43
|
+
(To upgrade from an older version, use `pip install --upgrade`)
|
|
43
44
|
|
|
44
45
|
Or download the latest release from Codeberg and run:
|
|
45
46
|
```
|
|
@@ -72,7 +73,13 @@ path/to/Splatlogger-1.x.exe -ip IP [options]
|
|
|
72
73
|
```
|
|
73
74
|
|
|
74
75
|
Options:
|
|
75
|
-
- `-
|
|
76
|
+
- `-aroma` - Switch to Aroma mode. Enable if using the TCPGecko Aroma plugin.
|
|
77
|
+
|
|
78
|
+
- `-cemu [PID]` - Switch to Cemu mode. The `-ip` argument is not required.
|
|
79
|
+
Note: If you have ptrace protection enabled, you'll need to run Python as root. It's best to run the module from source in that case.
|
|
80
|
+
- `PID` - PID of the Cemu process (optional).
|
|
81
|
+
|
|
82
|
+
- `-log-level option` - Set how much data should be logged.
|
|
76
83
|
- `none` - Don't create a log file.
|
|
77
84
|
- `standard` - Log only basic player information, the same as what's printed to the console (default).
|
|
78
85
|
- `extended` - Log all player information and additional match information.
|
|
@@ -82,20 +89,18 @@ Options:
|
|
|
82
89
|
- `all` - Save a log of all matches you play (default).
|
|
83
90
|
- `latest` - Save a log of only the latest match.
|
|
84
91
|
|
|
85
|
-
- `-aroma` - Switch to Aroma mode. Enable if using the TCPGecko Aroma plugin.
|
|
86
|
-
|
|
87
|
-
- `-cemu [PID]` - Switch to Cemu mode. The `-ip` argument is not required and will be ignored.
|
|
88
|
-
*Note: If you have ptrace protection enabled, you'll need to run python as root. It's best to run the module from source in that case.*
|
|
89
|
-
- `PID` - PID of the Cemu process (optional).
|
|
90
|
-
|
|
91
92
|
- `-stack Address (in hex)` - (Cemu only, optional) Beginning address of the stack space for Default Core 1 (Debug > View PPC threads).
|
|
92
93
|
Only required for logging stats. If the default values don't work, you'll need to specify this manually.
|
|
93
94
|
|
|
94
95
|
<img src="stack.png" width="600" alt="(example)">
|
|
95
96
|
|
|
97
|
+
- `-spfn` - Fetch SFID instead of PNID. Enable if playing on SPFN.
|
|
98
|
+
|
|
99
|
+
- `-no-pnid` - Disable fetching PNIDs.
|
|
100
|
+
|
|
96
101
|
- `-silent` - Disable printing logs to the console.
|
|
97
102
|
|
|
98
|
-
Logs are saved in `(User)/Documents/Splatlogger/logs
|
|
103
|
+
Logs are saved in `(User)/Documents/Splatlogger/logs/`.
|
|
99
104
|
|
|
100
105
|
To always run with the same IP and options without having to type them in manually, create an `args.txt` file in
|
|
101
106
|
`(User)/Documents/Splatlogger/` and put the arguments following `splatlogger` in there.
|
|
@@ -104,6 +109,8 @@ Afterward any arguments passed through the command line will be ignored.
|
|
|
104
109
|
Only one program can be connected to TCPGecko at a time. If you have something else connected, disconnect it beforehand.
|
|
105
110
|
|
|
106
111
|
## Credits
|
|
107
|
-
- [pyGecko](https://github.com/wiiudev/pyGecko) authors for
|
|
112
|
+
- [pyGecko](https://github.com/wiiudev/pyGecko) authors for the reference implementation of a Python TCPGecko library.
|
|
108
113
|
|
|
109
114
|
- Everyone who contributed to [PNIDGrab](https://github.com/JerrySM64/PNIDGrab) and other similar PID grabbers as those were used as a reference.
|
|
115
|
+
|
|
116
|
+
- Special thanks to Pirlo for helping me with certain stuff.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
MANIFEST.in
|
|
1
2
|
README.md
|
|
2
3
|
pyproject.toml
|
|
3
4
|
src/Splatlogger.egg-info/PKG-INFO
|
|
@@ -9,7 +10,10 @@ src/Splatlogger.egg-info/top_level.txt
|
|
|
9
10
|
src/splatlogger/__init__.py
|
|
10
11
|
src/splatlogger/__main__.py
|
|
11
12
|
src/splatlogger/cemu_gecko.py
|
|
12
|
-
src/splatlogger/
|
|
13
|
+
src/splatlogger/enums.py
|
|
13
14
|
src/splatlogger/match_logger.py
|
|
14
15
|
src/splatlogger/tcpgecko.py
|
|
15
|
-
src/splatlogger/tcpgecko_aroma.py
|
|
16
|
+
src/splatlogger/tcpgecko_aroma.py
|
|
17
|
+
src/splatlogger/tuples.py
|
|
18
|
+
src/splatlogger/data/api.json
|
|
19
|
+
src/splatlogger/data/names.json
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
# Splatlogger (c) 2025 Shadow Doggo.
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import binascii
|
|
5
|
+
import ipaddress
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
import webbrowser
|
|
10
|
+
from base64 import b64decode
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from importlib import resources
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Optional
|
|
15
|
+
from xml.etree import ElementTree
|
|
16
|
+
|
|
17
|
+
import requests
|
|
18
|
+
import userpaths # type: ignore[import-untyped]
|
|
19
|
+
|
|
20
|
+
from .cemu_gecko import CemuGecko, CemuGeckoError
|
|
21
|
+
from .enums import Pointers, Offsets
|
|
22
|
+
from .match_logger import MatchLogger
|
|
23
|
+
from .tcpgecko import TCPGecko, TCPGeckoError, to_hex, GeckoConnectionFailedError
|
|
24
|
+
from .tcpgecko_aroma import TCPGeckoAroma
|
|
25
|
+
from .tuples import Options, CommonAddresses, AccountInfo, PlayerInfo
|
|
26
|
+
|
|
27
|
+
_VERSION = "1.6.3"
|
|
28
|
+
_OPTIONS: Options
|
|
29
|
+
_GECKO: TCPGecko | TCPGeckoAroma | CemuGecko
|
|
30
|
+
_STATIC_MEM_ADR: int
|
|
31
|
+
_API: dict[str, str]
|
|
32
|
+
with resources.path(__package__, "data") as data_path:
|
|
33
|
+
try:
|
|
34
|
+
with open(data_path / "api.json", "r", encoding="utf-8") as api_f:
|
|
35
|
+
_API = json.load(api_f)
|
|
36
|
+
except FileNotFoundError:
|
|
37
|
+
print("Couldn't find 'api.json'. The package may have been installed incorrectly.")
|
|
38
|
+
sys.exit()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def main() -> None:
|
|
42
|
+
print(f"Splatlogger v{_VERSION} by Shadow Doggo\n")
|
|
43
|
+
|
|
44
|
+
parser = argparse.ArgumentParser(description="PID (Principal ID)/PNID grabber and match logger for Splatoon.")
|
|
45
|
+
parser.add_argument("-ip", help="Your Wii U's LAN IP address.")
|
|
46
|
+
parser.add_argument("-aroma", help="Switch to Aroma mode. Enable if using the TCPGecko Aroma plugin.",
|
|
47
|
+
action="store_true")
|
|
48
|
+
parser.add_argument("-cemu",
|
|
49
|
+
help="Switch to Cemu mode (optional: PID of the Cemu process).", metavar="PID",
|
|
50
|
+
nargs="?", const=-1)
|
|
51
|
+
parser.add_argument("-log-level", help="Set how much data should be logged (see README).",
|
|
52
|
+
choices=["none", "standard", "extended", "stats"], default="standard")
|
|
53
|
+
parser.add_argument("-auto",
|
|
54
|
+
help="Enable auto logging (see README).",
|
|
55
|
+
choices=["all", "latest"], nargs="?", const="all")
|
|
56
|
+
parser.add_argument("-stack",
|
|
57
|
+
help="Beginning address of the stack space for Default Core 1 (Cemu only, see README).",
|
|
58
|
+
metavar="Address (in hex)")
|
|
59
|
+
parser.add_argument("-spfn", help="Fetch SFID instead of PNID. Enable if playing on SPFN.",
|
|
60
|
+
action="store_true")
|
|
61
|
+
parser.add_argument("-no-pnid", help="Disable fetching PNIDs.", action="store_true")
|
|
62
|
+
parser.add_argument("-silent", help="Disable printing logs to the console.", action="store_true")
|
|
63
|
+
|
|
64
|
+
if (args_path := Path(userpaths.get_my_documents()) / "Splatlogger/args.txt").is_file():
|
|
65
|
+
with open(args_path, encoding="utf-8") as args_f:
|
|
66
|
+
args = parser.parse_args(args_f.read().split())
|
|
67
|
+
else:
|
|
68
|
+
args = parser.parse_args()
|
|
69
|
+
|
|
70
|
+
if len(sys.argv) == 1 and not args_path.is_file():
|
|
71
|
+
print('No arguments were provided. Run "splatlogger -h" for help.'
|
|
72
|
+
"\nWould you like to open the README? (y/n)")
|
|
73
|
+
if input().lower() in ("y", "yes"):
|
|
74
|
+
webbrowser.open("https://codeberg.org/ShadowDoggo/Splatlogger/src/branch/main/README.md")
|
|
75
|
+
|
|
76
|
+
sys.exit()
|
|
77
|
+
|
|
78
|
+
if args.aroma and args.cemu:
|
|
79
|
+
print("Cannot use both Aroma and Cemu modes.")
|
|
80
|
+
sys.exit()
|
|
81
|
+
|
|
82
|
+
stack_addr: int | None = None
|
|
83
|
+
if args.stack and args.cemu:
|
|
84
|
+
try:
|
|
85
|
+
stack_addr = int(args.stack, 16)
|
|
86
|
+
except ValueError:
|
|
87
|
+
print("Invalid stack address. Please provide a valid hex value.")
|
|
88
|
+
sys.exit()
|
|
89
|
+
|
|
90
|
+
global _OPTIONS
|
|
91
|
+
_OPTIONS = Options(log_level=args.log_level,
|
|
92
|
+
auto_logging=args.auto in ("all", "latest"),
|
|
93
|
+
log_latest=args.auto == "latest",
|
|
94
|
+
aroma=args.aroma,
|
|
95
|
+
cemu=args.cemu is not None,
|
|
96
|
+
spfn=args.spfn,
|
|
97
|
+
no_pnid=args.no_pnid,
|
|
98
|
+
silent_logging=args.silent,
|
|
99
|
+
stack=stack_addr)
|
|
100
|
+
|
|
101
|
+
ptr_offset = 0
|
|
102
|
+
global _GECKO
|
|
103
|
+
if args.cemu:
|
|
104
|
+
ptr_offset = 0x503000
|
|
105
|
+
try:
|
|
106
|
+
print("Opening Cemu process...")
|
|
107
|
+
_GECKO = CemuGecko(int(args.cemu))
|
|
108
|
+
except CemuGeckoError as e:
|
|
109
|
+
print(e)
|
|
110
|
+
sys.exit()
|
|
111
|
+
else:
|
|
112
|
+
try:
|
|
113
|
+
ipaddress.ip_address(args.ip)
|
|
114
|
+
print("Connecting to:", args.ip)
|
|
115
|
+
_GECKO = TCPGeckoAroma(args.ip) if _OPTIONS.aroma else TCPGecko(args.ip)
|
|
116
|
+
except ValueError:
|
|
117
|
+
print("Please provide a valid IP address.")
|
|
118
|
+
sys.exit()
|
|
119
|
+
except GeckoConnectionFailedError as e:
|
|
120
|
+
print(e)
|
|
121
|
+
sys.exit()
|
|
122
|
+
|
|
123
|
+
global _STATIC_MEM_ADR
|
|
124
|
+
_STATIC_MEM_ADR = 0
|
|
125
|
+
for _ in range(3):
|
|
126
|
+
try:
|
|
127
|
+
_STATIC_MEM_ADR = _GECKO.peek32(Pointers.STATIC_MEM - ptr_offset)
|
|
128
|
+
assert _STATIC_MEM_ADR != 0
|
|
129
|
+
|
|
130
|
+
print("Success.\n" if _OPTIONS.cemu else "Connected.\n")
|
|
131
|
+
break
|
|
132
|
+
except (OSError, TCPGeckoError) as e:
|
|
133
|
+
print(f"\nAn error has occurred: {e}\nRetrying in 10 seconds...")
|
|
134
|
+
time.sleep(10)
|
|
135
|
+
except AssertionError:
|
|
136
|
+
print("\nCould not find StaticMem address."
|
|
137
|
+
"\nIf you're on Cemu, this is likely due to the program not being able to read process memory",
|
|
138
|
+
"due to ptrace protection."
|
|
139
|
+
"\nRetrying in 10 seconds...")
|
|
140
|
+
time.sleep(10)
|
|
141
|
+
else:
|
|
142
|
+
_exit_with_close("\nFailed after 3 tries.")
|
|
143
|
+
|
|
144
|
+
match_logger: MatchLogger | None = None
|
|
145
|
+
if _OPTIONS.log_level != "none":
|
|
146
|
+
try:
|
|
147
|
+
match_logger = MatchLogger(_OPTIONS, _GECKO, _STATIC_MEM_ADR)
|
|
148
|
+
except FileNotFoundError:
|
|
149
|
+
_exit_with_close("Couldn't find 'names.json'. The package may have been installed incorrectly.")
|
|
150
|
+
if match_logger:
|
|
151
|
+
try:
|
|
152
|
+
match_logger.create_new_log()
|
|
153
|
+
except OSError as e:
|
|
154
|
+
_exit_with_close(f"Failed to create log file: {e}")
|
|
155
|
+
|
|
156
|
+
if _OPTIONS.auto_logging:
|
|
157
|
+
print("Auto logging enabled.")
|
|
158
|
+
_auto_log(match_logger)
|
|
159
|
+
|
|
160
|
+
for _ in range(3):
|
|
161
|
+
try:
|
|
162
|
+
_splatlog(match_logger, match_count=1)
|
|
163
|
+
break
|
|
164
|
+
except (OSError, TCPGeckoError) as e:
|
|
165
|
+
print(f"\nAn error has occurred: {e}\nRetrying in 10 seconds...")
|
|
166
|
+
time.sleep(10)
|
|
167
|
+
else:
|
|
168
|
+
_exit_with_close("\nFailed after 3 tries.")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _auto_log(match_logger: MatchLogger) -> None:
|
|
172
|
+
match_in_progress = False
|
|
173
|
+
count = 0
|
|
174
|
+
retry_count = 0
|
|
175
|
+
ptr_offset = 0x503000 if _OPTIONS.cemu else 0
|
|
176
|
+
while True:
|
|
177
|
+
try:
|
|
178
|
+
main_mgr_vs_game_adr = _GECKO.peek32(Pointers.MAIN_MGR_VS_GAME - ptr_offset)
|
|
179
|
+
if main_mgr_vs_game_adr == 0:
|
|
180
|
+
match_in_progress = False
|
|
181
|
+
|
|
182
|
+
# MainMgrVSGame is initialized when a VS match starts.
|
|
183
|
+
if not match_in_progress and main_mgr_vs_game_adr != 0:
|
|
184
|
+
match_in_progress = True
|
|
185
|
+
count += 1
|
|
186
|
+
|
|
187
|
+
if _OPTIONS.log_latest:
|
|
188
|
+
match_logger.create_new_log()
|
|
189
|
+
|
|
190
|
+
if not _OPTIONS.silent_logging:
|
|
191
|
+
print(f"\nMatch {count}\n")
|
|
192
|
+
|
|
193
|
+
time.sleep(1)
|
|
194
|
+
|
|
195
|
+
for _ in range(3):
|
|
196
|
+
try:
|
|
197
|
+
_splatlog(match_logger, count)
|
|
198
|
+
break
|
|
199
|
+
except (OSError, TCPGeckoError) as e:
|
|
200
|
+
print(f"\nAn error has occurred: {e}\nRetrying in 10 seconds...")
|
|
201
|
+
time.sleep(10)
|
|
202
|
+
else:
|
|
203
|
+
_exit_with_close("\nFailed after 3 tries.")
|
|
204
|
+
|
|
205
|
+
retry_count = 0
|
|
206
|
+
time.sleep(10)
|
|
207
|
+
except KeyboardInterrupt:
|
|
208
|
+
_exit_with_close("\nExiting.")
|
|
209
|
+
except (OSError, TCPGeckoError) as e:
|
|
210
|
+
if retry_count < 3:
|
|
211
|
+
print(f"\nAn error has occurred: {e}\nRetrying in 10 seconds...")
|
|
212
|
+
retry_count += 1
|
|
213
|
+
time.sleep(10)
|
|
214
|
+
else:
|
|
215
|
+
_exit_with_close("\nFailed after 3 tries.")
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _splatlog(match_logger: MatchLogger | None, match_count: int) -> None:
|
|
219
|
+
session_id = 0
|
|
220
|
+
player_count = 8
|
|
221
|
+
request_errors: list[str] = []
|
|
222
|
+
player_info_list: list[PlayerInfo] = []
|
|
223
|
+
date = datetime.now()
|
|
224
|
+
ptr_offset = 0x503000 if _OPTIONS.cemu else 0
|
|
225
|
+
|
|
226
|
+
if (session_adr := _GECKO.peek32(Pointers.SESSION - ptr_offset)) != 0:
|
|
227
|
+
session_id_idx = int.from_bytes(_GECKO.peek_raw(session_adr + Offsets.SESSION_ID_IDX,
|
|
228
|
+
length=1), byteorder="big")
|
|
229
|
+
session_id = _GECKO.peek32(session_adr + session_id_idx * 4 + Offsets.SESSION_ID)
|
|
230
|
+
|
|
231
|
+
main_mgr_base_adr = _GECKO.peek32(Pointers.MAIN_MGR_BASE - ptr_offset)
|
|
232
|
+
if _GECKO.peek32(Pointers.MAIN_MGR_VS_GAME - ptr_offset) != 0 or _OPTIONS.auto_logging:
|
|
233
|
+
if (player_mgr_adr := _GECKO.peek32(main_mgr_base_adr + Offsets.MAINMGR_PLAYER_MGR)) != 0:
|
|
234
|
+
player_count = _GECKO.peek8(player_mgr_adr + Offsets.PLAYERMGR_PLAYER_COUNT)
|
|
235
|
+
else:
|
|
236
|
+
print("You are currently not in a match. Logging previous match data "
|
|
237
|
+
"(Player 1 data will be your own, Session ID will be the current one).\n")
|
|
238
|
+
|
|
239
|
+
player_info_ary_adr = _GECKO.peek32(_STATIC_MEM_ADR + Offsets.STATICMEM_PLAYER_INFO_ARY)
|
|
240
|
+
assert player_info_ary_adr != 0, "PlayerInfoAry pointer is null."
|
|
241
|
+
|
|
242
|
+
network_id_type = "SFID" if _OPTIONS.spfn else "PNID"
|
|
243
|
+
print(f"{'Player':<8} | {'PID (Hex)':<10} | {'PID (Dec)':<10} | {network_id_type:<16} | Name\n"
|
|
244
|
+
f"{'-' * 73}")
|
|
245
|
+
|
|
246
|
+
for player_idx in range(player_count):
|
|
247
|
+
player_info_adr = _GECKO.peek32(player_info_ary_adr + player_idx * 0x4)
|
|
248
|
+
if player_info_adr == 0:
|
|
249
|
+
continue
|
|
250
|
+
|
|
251
|
+
player_info_raw = _GECKO.peek_raw(player_info_adr, length=0xD4)
|
|
252
|
+
|
|
253
|
+
player_pid = int.from_bytes(player_info_raw[0xD0:0xD4], byteorder="big")
|
|
254
|
+
if player_pid != 0:
|
|
255
|
+
player_name = (player_info_raw[0x6:0x26]
|
|
256
|
+
.decode("utf-16-be")
|
|
257
|
+
.split("\u0000")[0]
|
|
258
|
+
.replace("\n", "")
|
|
259
|
+
.replace("\r", ""))
|
|
260
|
+
|
|
261
|
+
if not _OPTIONS.no_pnid:
|
|
262
|
+
act_info = _get_spfn_act_info(player_pid) if _OPTIONS.spfn else _get_act_info(player_pid)
|
|
263
|
+
if act_info.request_error != "":
|
|
264
|
+
request_errors.append(act_info.request_error)
|
|
265
|
+
act_info = AccountInfo("", player_name, "")
|
|
266
|
+
else:
|
|
267
|
+
act_info = AccountInfo("", player_name, "")
|
|
268
|
+
|
|
269
|
+
if _OPTIONS.log_level != "none":
|
|
270
|
+
player_info_list.append(PlayerInfo(
|
|
271
|
+
idx=player_idx,
|
|
272
|
+
pid=player_pid,
|
|
273
|
+
pnid=act_info.pnid,
|
|
274
|
+
name=player_name,
|
|
275
|
+
mii_name=act_info.mii_name,
|
|
276
|
+
team=int.from_bytes(player_info_raw[0x33:0x34], byteorder="big"),
|
|
277
|
+
gender=int.from_bytes(player_info_raw[0x37:0x38], byteorder="big"),
|
|
278
|
+
skin_tone=int.from_bytes(player_info_raw[0x3B:0x3C], byteorder="big"),
|
|
279
|
+
eye_color=int.from_bytes(player_info_raw[0x3F:0x40], byteorder="big"),
|
|
280
|
+
weapon=int.from_bytes(player_info_raw[0x46:0x48], byteorder="big"),
|
|
281
|
+
sub_weapon=int.from_bytes(player_info_raw[0x4A:0x4C], byteorder="big"),
|
|
282
|
+
special_weapon=int.from_bytes(player_info_raw[0x4D:0x50], byteorder="big"),
|
|
283
|
+
shoes=int.from_bytes(player_info_raw[0x54:0x58], byteorder="big"),
|
|
284
|
+
clothes=int.from_bytes(player_info_raw[0x70:0x74], byteorder="big"),
|
|
285
|
+
headgear=int.from_bytes(player_info_raw[0x8C:0x90], byteorder="big"),
|
|
286
|
+
level=int.from_bytes(player_info_raw[0xAF:0xB0], byteorder="big", signed=True) + 1,
|
|
287
|
+
rank=int.from_bytes(player_info_raw[0xB3:0xB4], byteorder="big", signed=True)
|
|
288
|
+
))
|
|
289
|
+
|
|
290
|
+
if not _OPTIONS.silent_logging:
|
|
291
|
+
print(f"Player {player_idx + 1} | "
|
|
292
|
+
f"{to_hex(player_pid):<8} | "
|
|
293
|
+
f"{player_pid:<10} | "
|
|
294
|
+
f"{act_info.pnid:<16} | "
|
|
295
|
+
f"{player_name} {f'({act_info.mii_name})' if player_name != act_info.mii_name else ''}")
|
|
296
|
+
|
|
297
|
+
if not _OPTIONS.silent_logging:
|
|
298
|
+
print()
|
|
299
|
+
if request_errors:
|
|
300
|
+
print(f"Failed to get account data for one or more players: {'; '.join(request_errors)}\n")
|
|
301
|
+
|
|
302
|
+
if session_id != 0:
|
|
303
|
+
print(f"Session ID: {to_hex(session_id)} ({session_id})")
|
|
304
|
+
else:
|
|
305
|
+
print("Session ID: None")
|
|
306
|
+
|
|
307
|
+
print(f"Fetched at: {date.strftime('%Y-%m-%d %H:%M:%S')} ({date.astimezone().tzname()})")
|
|
308
|
+
|
|
309
|
+
if _OPTIONS.log_level != "none" and match_logger:
|
|
310
|
+
adrs = CommonAddresses(main_mgr_base_adr, player_info_ary_adr)
|
|
311
|
+
match_logger.log_match(date, session_id, match_count, player_info_list, adrs)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _get_act_info(player_pid: int) -> AccountInfo:
|
|
315
|
+
pnid = ""
|
|
316
|
+
mii_name = ""
|
|
317
|
+
request_error = ""
|
|
318
|
+
|
|
319
|
+
for _ in range(3):
|
|
320
|
+
try:
|
|
321
|
+
response = requests.get(
|
|
322
|
+
f"https://account.pretendo.cc/v1/api/miis?pids={player_pid}",
|
|
323
|
+
headers={"X-Nintendo-Client-ID": _API["client_id"],
|
|
324
|
+
"X-Nintendo-Client-Secret": _API["client_secret"]},
|
|
325
|
+
timeout=5
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
try:
|
|
329
|
+
root = ElementTree.fromstring(response.text)
|
|
330
|
+
except ElementTree.ParseError:
|
|
331
|
+
return AccountInfo("", "", "Got empty or invalid response.")
|
|
332
|
+
|
|
333
|
+
if not root or len(root) == 0:
|
|
334
|
+
return AccountInfo("", "", "Got empty or invalid response.")
|
|
335
|
+
|
|
336
|
+
if not (isinstance(root[0].find("user_id"), ElementTree.Element)
|
|
337
|
+
and isinstance(root[0].find("name"), ElementTree.Element)):
|
|
338
|
+
return AccountInfo("", "", "Got empty or invalid response.")
|
|
339
|
+
|
|
340
|
+
pnid = root[0].find("user_id").text # type: ignore
|
|
341
|
+
mii_name = (root[0].find("name") # type: ignore
|
|
342
|
+
.text
|
|
343
|
+
.replace("\n", "")
|
|
344
|
+
.replace("\r", ""))
|
|
345
|
+
break
|
|
346
|
+
except requests.RequestException as ex:
|
|
347
|
+
request_error = str(ex)
|
|
348
|
+
time.sleep(1)
|
|
349
|
+
|
|
350
|
+
return AccountInfo(pnid, mii_name, request_error) # type: ignore
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _get_spfn_act_info(player_pid: int) -> AccountInfo:
|
|
354
|
+
pnid = ""
|
|
355
|
+
mii_name = ""
|
|
356
|
+
request_error = ""
|
|
357
|
+
|
|
358
|
+
for _ in range(3):
|
|
359
|
+
try:
|
|
360
|
+
response = requests.get(
|
|
361
|
+
f"https://account.spfn.net/v1/api/miis?pids={player_pid}",
|
|
362
|
+
timeout=5
|
|
363
|
+
)
|
|
364
|
+
mii_data_response = requests.get(
|
|
365
|
+
f"https://account.spfn.net/api/v2/users/{player_pid}/mii",
|
|
366
|
+
timeout=5
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
try:
|
|
370
|
+
root = ElementTree.fromstring(response.text)
|
|
371
|
+
except ElementTree.ParseError:
|
|
372
|
+
return AccountInfo("", "", "Got empty or invalid response.")
|
|
373
|
+
|
|
374
|
+
if not root or len(root) == 0:
|
|
375
|
+
return AccountInfo("", "", "Got empty or invalid response.")
|
|
376
|
+
|
|
377
|
+
if not isinstance(root[0].find("user_id"), ElementTree.Element):
|
|
378
|
+
return AccountInfo("", "", "Got empty or invalid response.")
|
|
379
|
+
|
|
380
|
+
pnid = root[0].find("user_id").text # type: ignore
|
|
381
|
+
|
|
382
|
+
try:
|
|
383
|
+
mii_data = b64decode(mii_data_response.text.replace("\\n", "").replace("\\r", ""))
|
|
384
|
+
mii_name = mii_data.decode("utf-16")[13:23]
|
|
385
|
+
except (binascii.Error, UnicodeDecodeError):
|
|
386
|
+
return AccountInfo("", "", "Got empty or invalid response.")
|
|
387
|
+
|
|
388
|
+
break
|
|
389
|
+
except requests.RequestException as ex:
|
|
390
|
+
request_error = str(ex)
|
|
391
|
+
time.sleep(1)
|
|
392
|
+
|
|
393
|
+
return AccountInfo(pnid, mii_name, request_error) # type: ignore
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _exit_with_close(message: Optional[str] = None) -> None:
|
|
397
|
+
if message:
|
|
398
|
+
print(message)
|
|
399
|
+
_GECKO.close()
|
|
400
|
+
sys.exit()
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
if __name__ == "__main__":
|
|
404
|
+
main()
|