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.
Files changed (26) hide show
  1. splatlogger-1.6.3/MANIFEST.in +1 -0
  2. {splatlogger-1.6.2 → splatlogger-1.6.3}/PKG-INFO +19 -12
  3. {splatlogger-1.6.2 → splatlogger-1.6.3}/README.md +17 -10
  4. {splatlogger-1.6.2 → splatlogger-1.6.3}/pyproject.toml +2 -2
  5. {splatlogger-1.6.2 → splatlogger-1.6.3}/src/Splatlogger.egg-info/PKG-INFO +19 -12
  6. {splatlogger-1.6.2 → splatlogger-1.6.3}/src/Splatlogger.egg-info/SOURCES.txt +6 -2
  7. splatlogger-1.6.3/src/splatlogger/__main__.py +404 -0
  8. {splatlogger-1.6.2 → splatlogger-1.6.3}/src/splatlogger/cemu_gecko.py +19 -9
  9. splatlogger-1.6.3/src/splatlogger/data/api.json +4 -0
  10. splatlogger-1.6.3/src/splatlogger/data/names.json +725 -0
  11. splatlogger-1.6.3/src/splatlogger/enums.py +33 -0
  12. splatlogger-1.6.3/src/splatlogger/match_logger.py +210 -0
  13. splatlogger-1.6.3/src/splatlogger/tcpgecko.py +159 -0
  14. splatlogger-1.6.3/src/splatlogger/tcpgecko_aroma.py +109 -0
  15. splatlogger-1.6.3/src/splatlogger/tuples.py +46 -0
  16. splatlogger-1.6.2/src/splatlogger/__main__.py +0 -337
  17. splatlogger-1.6.2/src/splatlogger/data.py +0 -764
  18. splatlogger-1.6.2/src/splatlogger/match_logger.py +0 -185
  19. splatlogger-1.6.2/src/splatlogger/tcpgecko.py +0 -162
  20. splatlogger-1.6.2/src/splatlogger/tcpgecko_aroma.py +0 -116
  21. {splatlogger-1.6.2 → splatlogger-1.6.3}/setup.cfg +0 -0
  22. {splatlogger-1.6.2 → splatlogger-1.6.3}/src/Splatlogger.egg-info/dependency_links.txt +0 -0
  23. {splatlogger-1.6.2 → splatlogger-1.6.3}/src/Splatlogger.egg-info/entry_points.txt +0 -0
  24. {splatlogger-1.6.2 → splatlogger-1.6.3}/src/Splatlogger.egg-info/requires.txt +0 -0
  25. {splatlogger-1.6.2 → splatlogger-1.6.3}/src/Splatlogger.egg-info/top_level.txt +0 -0
  26. {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.2
4
- Summary: A CLI PID (Principal ID)/PNID grabber and match logger for Splatoon.
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
- - `-log-level [option]` - Set how much data should be logged.
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/(Date)/`.
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 most of `tcpgecko.py`.
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
- - `-log-level [option]` - Set how much data should be logged.
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/(Date)/`.
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 most of `tcpgecko.py`.
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.2"
7
+ version = "1.6.3"
8
8
  authors = [
9
9
  { name="Shadow Doggo", email="shadowdoggo@protonmail.com" }
10
10
  ]
11
- description = "A CLI PID (Principal ID)/PNID grabber and match logger for Splatoon."
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.2
4
- Summary: A CLI PID (Principal ID)/PNID grabber and match logger for Splatoon.
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
- - `-log-level [option]` - Set how much data should be logged.
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/(Date)/`.
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 most of `tcpgecko.py`.
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/data.py
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()