Splatlogger 1.2.3__tar.gz → 1.3.0__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.2.3/src/Splatlogger.egg-info → splatlogger-1.3.0}/PKG-INFO +8 -11
- {splatlogger-1.2.3 → splatlogger-1.3.0}/README.md +7 -10
- {splatlogger-1.2.3 → splatlogger-1.3.0}/pyproject.toml +1 -1
- {splatlogger-1.2.3 → splatlogger-1.3.0/src/Splatlogger.egg-info}/PKG-INFO +8 -11
- {splatlogger-1.2.3 → splatlogger-1.3.0}/src/splatlogger/__init__.py +1 -1
- {splatlogger-1.2.3 → splatlogger-1.3.0}/src/splatlogger/__main__.py +0 -1
- {splatlogger-1.2.3 → splatlogger-1.3.0}/src/splatlogger/cli.py +17 -26
- splatlogger-1.3.0/src/splatlogger/enums.py +137 -0
- {splatlogger-1.2.3 → splatlogger-1.3.0}/src/splatlogger/match_logger.py +164 -114
- {splatlogger-1.2.3 → splatlogger-1.3.0}/src/splatlogger/resources/names.json +3 -4
- {splatlogger-1.2.3 → splatlogger-1.3.0}/src/splatlogger/splatlogger.py +57 -39
- {splatlogger-1.2.3 → splatlogger-1.3.0}/src/splatlogger/tuples.py +19 -1
- {splatlogger-1.2.3 → splatlogger-1.3.0}/src/splatlogger/utility_functions.py +7 -10
- splatlogger-1.2.3/src/splatlogger/enums.py +0 -81
- {splatlogger-1.2.3 → splatlogger-1.3.0}/LICENSE +0 -0
- {splatlogger-1.2.3 → splatlogger-1.3.0}/setup.cfg +0 -0
- {splatlogger-1.2.3 → splatlogger-1.3.0}/src/Splatlogger.egg-info/SOURCES.txt +0 -0
- {splatlogger-1.2.3 → splatlogger-1.3.0}/src/Splatlogger.egg-info/dependency_links.txt +0 -0
- {splatlogger-1.2.3 → splatlogger-1.3.0}/src/Splatlogger.egg-info/entry_points.txt +0 -0
- {splatlogger-1.2.3 → splatlogger-1.3.0}/src/Splatlogger.egg-info/requires.txt +0 -0
- {splatlogger-1.2.3 → splatlogger-1.3.0}/src/Splatlogger.egg-info/top_level.txt +0 -0
- {splatlogger-1.2.3 → splatlogger-1.3.0}/src/splatlogger/cemu_gecko.py +0 -0
- {splatlogger-1.2.3 → splatlogger-1.3.0}/src/splatlogger/errors.py +0 -0
- {splatlogger-1.2.3 → splatlogger-1.3.0}/src/splatlogger/tcpgecko.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: Splatlogger
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: PID Grabber and player/match information logging tool for Splatoon
|
|
5
5
|
Author-email: Shadow Doggo <shadowdoggo@protonmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -74,6 +74,7 @@ To enable Cemu support, install with the `cemu` extra:
|
|
|
74
74
|
```
|
|
75
75
|
pip install "Splatlogger[cemu]"
|
|
76
76
|
```
|
|
77
|
+
(use `pip install --upgrade` to update from an older version)
|
|
77
78
|
|
|
78
79
|
Then run with:
|
|
79
80
|
```
|
|
@@ -102,7 +103,7 @@ python -m splatlogger --ip IP [options] / --cemu [PID] [options]
|
|
|
102
103
|
- `none` - Don't create a log file.
|
|
103
104
|
- `standard` - Log only basic player information, the same as what's printed to the console (default).
|
|
104
105
|
- `extended` - Log all player information and additional match information.
|
|
105
|
-
- `
|
|
106
|
+
- `full` - Same as above with the addition of player stats (points, K/D, win/lose), disconnect detection, and match results.
|
|
106
107
|
Requires the match to end to finish logging; if it ends abruptly, the stats won't be logged.
|
|
107
108
|
|
|
108
109
|
- `--service` - Choose which network service you're using.
|
|
@@ -117,11 +118,6 @@ Also allows you to view information about the lobby/gathering while on the match
|
|
|
117
118
|
|
|
118
119
|
(These options don't affect anything if the log level is set to `none`)
|
|
119
120
|
|
|
120
|
-
- `--stack Address (in hex)` - (Cemu only, optional) Beginning address of the stack space for Default Core 1 (Debug > View PPC threads).
|
|
121
|
-
Only required for logging stats. If the default values don't work, you'll need to specify this manually.
|
|
122
|
-
|
|
123
|
-
<img src="assets/stack.png" width="600" alt="(example)">
|
|
124
|
-
|
|
125
121
|
- `-sl`, `--single-log` - Enable single log mode. When enabled, only one log file will be created for each day and logging will resume in that file
|
|
126
122
|
instead of creating a new one for each logging session (default behavior for `--auto latest`).
|
|
127
123
|
|
|
@@ -149,7 +145,7 @@ ip = "192.168.1.50"
|
|
|
149
145
|
# Target network service: "pretendo", "altendo", or "none"
|
|
150
146
|
service = "pretendo"
|
|
151
147
|
|
|
152
|
-
# Log level: "standard", "extended", "
|
|
148
|
+
# Log level: "standard", "extended", "full", or "none"
|
|
153
149
|
log_level = "extended"
|
|
154
150
|
|
|
155
151
|
# Auto logging: "all", "latest" (omit for manual logging)
|
|
@@ -168,9 +164,6 @@ single_log = true
|
|
|
168
164
|
# Cemu process ID (set to -1 to find automatically)
|
|
169
165
|
# Uncomment this option to enable Cemu mode
|
|
170
166
|
# cemu = -1
|
|
171
|
-
|
|
172
|
-
# Beginning address of the stack space for Default Core 1 (set only if stat logging doesn't work by default)
|
|
173
|
-
# stack = "0xE101440"
|
|
174
167
|
```
|
|
175
168
|
|
|
176
169
|
### Troubleshooting
|
|
@@ -207,6 +200,10 @@ python scripts/build.py [--upx-dir path/to/upx]
|
|
|
207
200
|
```
|
|
208
201
|
use `--upx-dir` if you're using UPX to compress the executable.
|
|
209
202
|
|
|
203
|
+
You can also use `build.bat` on Windows and `build.sh` on Linux to handle everything automatically.
|
|
204
|
+
|
|
205
|
+
(No builds or build script are provided for macOS as I don't have a Mac to test, and my attempts at virtualizing macOS have been unsuccessful.)
|
|
206
|
+
|
|
210
207
|
## Acknowledgments
|
|
211
208
|
- NWPlayer123 for the reference implementation of a TCPGecko library.
|
|
212
209
|
|
|
@@ -49,6 +49,7 @@ To enable Cemu support, install with the `cemu` extra:
|
|
|
49
49
|
```
|
|
50
50
|
pip install "Splatlogger[cemu]"
|
|
51
51
|
```
|
|
52
|
+
(use `pip install --upgrade` to update from an older version)
|
|
52
53
|
|
|
53
54
|
Then run with:
|
|
54
55
|
```
|
|
@@ -77,7 +78,7 @@ python -m splatlogger --ip IP [options] / --cemu [PID] [options]
|
|
|
77
78
|
- `none` - Don't create a log file.
|
|
78
79
|
- `standard` - Log only basic player information, the same as what's printed to the console (default).
|
|
79
80
|
- `extended` - Log all player information and additional match information.
|
|
80
|
-
- `
|
|
81
|
+
- `full` - Same as above with the addition of player stats (points, K/D, win/lose), disconnect detection, and match results.
|
|
81
82
|
Requires the match to end to finish logging; if it ends abruptly, the stats won't be logged.
|
|
82
83
|
|
|
83
84
|
- `--service` - Choose which network service you're using.
|
|
@@ -92,11 +93,6 @@ Also allows you to view information about the lobby/gathering while on the match
|
|
|
92
93
|
|
|
93
94
|
(These options don't affect anything if the log level is set to `none`)
|
|
94
95
|
|
|
95
|
-
- `--stack Address (in hex)` - (Cemu only, optional) Beginning address of the stack space for Default Core 1 (Debug > View PPC threads).
|
|
96
|
-
Only required for logging stats. If the default values don't work, you'll need to specify this manually.
|
|
97
|
-
|
|
98
|
-
<img src="assets/stack.png" width="600" alt="(example)">
|
|
99
|
-
|
|
100
96
|
- `-sl`, `--single-log` - Enable single log mode. When enabled, only one log file will be created for each day and logging will resume in that file
|
|
101
97
|
instead of creating a new one for each logging session (default behavior for `--auto latest`).
|
|
102
98
|
|
|
@@ -124,7 +120,7 @@ ip = "192.168.1.50"
|
|
|
124
120
|
# Target network service: "pretendo", "altendo", or "none"
|
|
125
121
|
service = "pretendo"
|
|
126
122
|
|
|
127
|
-
# Log level: "standard", "extended", "
|
|
123
|
+
# Log level: "standard", "extended", "full", or "none"
|
|
128
124
|
log_level = "extended"
|
|
129
125
|
|
|
130
126
|
# Auto logging: "all", "latest" (omit for manual logging)
|
|
@@ -143,9 +139,6 @@ single_log = true
|
|
|
143
139
|
# Cemu process ID (set to -1 to find automatically)
|
|
144
140
|
# Uncomment this option to enable Cemu mode
|
|
145
141
|
# cemu = -1
|
|
146
|
-
|
|
147
|
-
# Beginning address of the stack space for Default Core 1 (set only if stat logging doesn't work by default)
|
|
148
|
-
# stack = "0xE101440"
|
|
149
142
|
```
|
|
150
143
|
|
|
151
144
|
### Troubleshooting
|
|
@@ -182,6 +175,10 @@ python scripts/build.py [--upx-dir path/to/upx]
|
|
|
182
175
|
```
|
|
183
176
|
use `--upx-dir` if you're using UPX to compress the executable.
|
|
184
177
|
|
|
178
|
+
You can also use `build.bat` on Windows and `build.sh` on Linux to handle everything automatically.
|
|
179
|
+
|
|
180
|
+
(No builds or build script are provided for macOS as I don't have a Mac to test, and my attempts at virtualizing macOS have been unsuccessful.)
|
|
181
|
+
|
|
185
182
|
## Acknowledgments
|
|
186
183
|
- NWPlayer123 for the reference implementation of a TCPGecko library.
|
|
187
184
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: Splatlogger
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: PID Grabber and player/match information logging tool for Splatoon
|
|
5
5
|
Author-email: Shadow Doggo <shadowdoggo@protonmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -74,6 +74,7 @@ To enable Cemu support, install with the `cemu` extra:
|
|
|
74
74
|
```
|
|
75
75
|
pip install "Splatlogger[cemu]"
|
|
76
76
|
```
|
|
77
|
+
(use `pip install --upgrade` to update from an older version)
|
|
77
78
|
|
|
78
79
|
Then run with:
|
|
79
80
|
```
|
|
@@ -102,7 +103,7 @@ python -m splatlogger --ip IP [options] / --cemu [PID] [options]
|
|
|
102
103
|
- `none` - Don't create a log file.
|
|
103
104
|
- `standard` - Log only basic player information, the same as what's printed to the console (default).
|
|
104
105
|
- `extended` - Log all player information and additional match information.
|
|
105
|
-
- `
|
|
106
|
+
- `full` - Same as above with the addition of player stats (points, K/D, win/lose), disconnect detection, and match results.
|
|
106
107
|
Requires the match to end to finish logging; if it ends abruptly, the stats won't be logged.
|
|
107
108
|
|
|
108
109
|
- `--service` - Choose which network service you're using.
|
|
@@ -117,11 +118,6 @@ Also allows you to view information about the lobby/gathering while on the match
|
|
|
117
118
|
|
|
118
119
|
(These options don't affect anything if the log level is set to `none`)
|
|
119
120
|
|
|
120
|
-
- `--stack Address (in hex)` - (Cemu only, optional) Beginning address of the stack space for Default Core 1 (Debug > View PPC threads).
|
|
121
|
-
Only required for logging stats. If the default values don't work, you'll need to specify this manually.
|
|
122
|
-
|
|
123
|
-
<img src="assets/stack.png" width="600" alt="(example)">
|
|
124
|
-
|
|
125
121
|
- `-sl`, `--single-log` - Enable single log mode. When enabled, only one log file will be created for each day and logging will resume in that file
|
|
126
122
|
instead of creating a new one for each logging session (default behavior for `--auto latest`).
|
|
127
123
|
|
|
@@ -149,7 +145,7 @@ ip = "192.168.1.50"
|
|
|
149
145
|
# Target network service: "pretendo", "altendo", or "none"
|
|
150
146
|
service = "pretendo"
|
|
151
147
|
|
|
152
|
-
# Log level: "standard", "extended", "
|
|
148
|
+
# Log level: "standard", "extended", "full", or "none"
|
|
153
149
|
log_level = "extended"
|
|
154
150
|
|
|
155
151
|
# Auto logging: "all", "latest" (omit for manual logging)
|
|
@@ -168,9 +164,6 @@ single_log = true
|
|
|
168
164
|
# Cemu process ID (set to -1 to find automatically)
|
|
169
165
|
# Uncomment this option to enable Cemu mode
|
|
170
166
|
# cemu = -1
|
|
171
|
-
|
|
172
|
-
# Beginning address of the stack space for Default Core 1 (set only if stat logging doesn't work by default)
|
|
173
|
-
# stack = "0xE101440"
|
|
174
167
|
```
|
|
175
168
|
|
|
176
169
|
### Troubleshooting
|
|
@@ -207,6 +200,10 @@ python scripts/build.py [--upx-dir path/to/upx]
|
|
|
207
200
|
```
|
|
208
201
|
use `--upx-dir` if you're using UPX to compress the executable.
|
|
209
202
|
|
|
203
|
+
You can also use `build.bat` on Windows and `build.sh` on Linux to handle everything automatically.
|
|
204
|
+
|
|
205
|
+
(No builds or build script are provided for macOS as I don't have a Mac to test, and my attempts at virtualizing macOS have been unsuccessful.)
|
|
206
|
+
|
|
210
207
|
## Acknowledgments
|
|
211
208
|
- NWPlayer123 for the reference implementation of a TCPGecko library.
|
|
212
209
|
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
__version__ = "1.
|
|
1
|
+
__version__ = "1.3.0"
|
|
2
2
|
__author__ = "Shadow Doggo"
|
|
@@ -7,7 +7,7 @@ import webbrowser
|
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
|
|
9
9
|
import userpaths
|
|
10
|
-
from colorama import
|
|
10
|
+
from colorama import just_fix_windows_console
|
|
11
11
|
from prompt_toolkit import PromptSession
|
|
12
12
|
from prompt_toolkit.patch_stdout import patch_stdout
|
|
13
13
|
|
|
@@ -70,7 +70,7 @@ def command_listener(ctx: AppContext) -> None:
|
|
|
70
70
|
|
|
71
71
|
|
|
72
72
|
def parse_args() -> Options:
|
|
73
|
-
parser = argparse.ArgumentParser(description="PID
|
|
73
|
+
parser = argparse.ArgumentParser(description="PID Grabber and player/match information logging tool for Splatoon")
|
|
74
74
|
add_parser_arguments(parser)
|
|
75
75
|
|
|
76
76
|
config_path = Path(userpaths.get_my_documents()) / "Splatlogger/config.toml"
|
|
@@ -83,28 +83,25 @@ def parse_args() -> Options:
|
|
|
83
83
|
args = parser.parse_args()
|
|
84
84
|
|
|
85
85
|
if not args.ip and not args.cemu:
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
"\nWould you like to open the README? (y/n)")
|
|
86
|
+
uf.print_warning("No target (IP address or Cemu mode) was provided. Run \"splatlogger -h\" for help.\n")
|
|
87
|
+
print("Would you like to open the README? (y/n)")
|
|
89
88
|
if input().lower() in ("y", "yes"):
|
|
90
89
|
webbrowser.open("https://codeberg.org/ShadowDoggo/Splatlogger/src/branch/main/README.md")
|
|
91
90
|
sys.exit()
|
|
92
91
|
|
|
93
|
-
stack_addr: int | None = None
|
|
94
|
-
if args.stack and args.cemu:
|
|
95
|
-
try:
|
|
96
|
-
stack_addr = int(args.stack, 16)
|
|
97
|
-
except ValueError:
|
|
98
|
-
uf.print_error("Invalid stack address. Please provide a valid hex value.")
|
|
99
|
-
sys.exit()
|
|
100
|
-
|
|
101
92
|
if args.log_level == "none" and args.silent:
|
|
102
|
-
uf.print_warning("
|
|
103
|
-
"Are you sure this wasn't a mistake?\n")
|
|
93
|
+
uf.print_warning("Silent mode is enabled and no log level is set. No information will be logged.\n")
|
|
104
94
|
|
|
105
|
-
return Options(
|
|
106
|
-
|
|
107
|
-
|
|
95
|
+
return Options(
|
|
96
|
+
ip=args.ip,
|
|
97
|
+
log_level=args.log_level,
|
|
98
|
+
auto_logging=args.auto in ("all", "latest"),
|
|
99
|
+
log_latest=args.auto == "latest",
|
|
100
|
+
platform="cemu" if args.cemu else "wiiu", cemu_pid=args.cemu,
|
|
101
|
+
service=args.service,
|
|
102
|
+
silent_logging=args.silent,
|
|
103
|
+
single_log=args.single_log
|
|
104
|
+
)
|
|
108
105
|
|
|
109
106
|
|
|
110
107
|
def add_parser_arguments(parser: argparse.ArgumentParser) -> None:
|
|
@@ -113,15 +110,12 @@ def add_parser_arguments(parser: argparse.ArgumentParser) -> None:
|
|
|
113
110
|
help="Switch to Cemu mode (optional: PID of the Cemu process).", metavar="PID",
|
|
114
111
|
nargs="?", const=-1)
|
|
115
112
|
parser.add_argument("--log-level", help="Set how much data should be logged (see README).",
|
|
116
|
-
choices=["none", "standard", "extended", "
|
|
113
|
+
choices=["none", "standard", "extended", "full"], default="standard")
|
|
117
114
|
parser.add_argument("--service", help="Choose which network service you're using (Pretendo by default).",
|
|
118
115
|
choices=["none", "pretendo", "altendo"], default="pretendo")
|
|
119
116
|
parser.add_argument("-a", "--auto",
|
|
120
117
|
help="Enable auto logging (see README).",
|
|
121
118
|
choices=["all", "latest"], nargs="?", const="all")
|
|
122
|
-
parser.add_argument("--stack",
|
|
123
|
-
help="Beginning address of the stack space for Default Core 1 (Cemu only, see README).",
|
|
124
|
-
metavar="Address (in hex)")
|
|
125
119
|
parser.add_argument("-sl", "--single-log", help="Enable single log file mode.", action="store_true")
|
|
126
120
|
parser.add_argument("-s", "--silent", help="Disable printing logs to the console.", action="store_true")
|
|
127
121
|
|
|
@@ -136,7 +130,7 @@ CONFIG_TEMPLATE = """# Splatlogger Configuration
|
|
|
136
130
|
# Target network service: "pretendo", "altendo", or "none"
|
|
137
131
|
# service = "pretendo"
|
|
138
132
|
|
|
139
|
-
# Log level: "standard", "extended", "
|
|
133
|
+
# Log level: "standard", "extended", "full", or "none"
|
|
140
134
|
# log_level = "standard"
|
|
141
135
|
|
|
142
136
|
# Auto logging: "all", "latest" (omit for manual logging)
|
|
@@ -155,9 +149,6 @@ CONFIG_TEMPLATE = """# Splatlogger Configuration
|
|
|
155
149
|
# Cemu process ID (set to -1 to find automatically)
|
|
156
150
|
# Uncomment this option to enable Cemu mode
|
|
157
151
|
# cemu = -1
|
|
158
|
-
|
|
159
|
-
# Beginning address of the stack space for Default Core 1 (set only if stat logging doesn't work by default)
|
|
160
|
-
# stack = "0xE101440"
|
|
161
152
|
"""
|
|
162
153
|
|
|
163
154
|
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Splatlogger (c) 2025 Shadow Doggo.
|
|
2
|
+
|
|
3
|
+
from enum import Enum, IntEnum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Pointers(IntEnum):
|
|
7
|
+
SESSION = 0x101E8980
|
|
8
|
+
STATIC_MEM = 0x101DD330
|
|
9
|
+
MAIN_MGR_BASE = 0x101E2814
|
|
10
|
+
MAIN_MGR_VS_GAME = 0x101E4FF8
|
|
11
|
+
FRAMEWORK = 0x101E80A8
|
|
12
|
+
|
|
13
|
+
def convert(self, platform: str) -> int:
|
|
14
|
+
return self.value + Offsets.WIIU_BSS_OFFSET if platform == "wiiu" else self.value
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Offsets(IntEnum):
|
|
18
|
+
WIIU_BSS_OFFSET = 0x503000
|
|
19
|
+
SESSION_ID_IDX = 0xBD
|
|
20
|
+
SESSION_ID = 0xCC
|
|
21
|
+
SESSION_ACTIVE_STATION_LIST_SENTINEL = 0xD8
|
|
22
|
+
SESSION_ACTIVE_STATION_LIST_FIRST_NODE_PTR = 0xDC
|
|
23
|
+
SESSION_ACTIVE_STATION_LIST_COUNT = 0xE0
|
|
24
|
+
SESSION_STATION_NODE_CAPACITY = 0xFC
|
|
25
|
+
STATION_LIST_NODE_PREV_PTR = 0x0
|
|
26
|
+
STATION_LIST_NODE_NEXT_PTR = 0x4
|
|
27
|
+
STATION_LIST_NODE_STATION_ID = 0x8
|
|
28
|
+
STATION_LIST_NODE_SIZE = 0x10
|
|
29
|
+
MAINMGR_PLAYER_MGR_PTR = 0x268
|
|
30
|
+
PLAYERMGR_PLAYER_COUNT = 0x320
|
|
31
|
+
MAINMGR_VS_GAME_SEQ_MGR_VS_GAME_PTR = 0x2A4
|
|
32
|
+
MAINMGR_VS_GAME_PAGE_HANDLER_PTR = 0x2D8
|
|
33
|
+
SEQ_MGR_VS_GAME_FINAL_RESULT_PTR = 0xAA8
|
|
34
|
+
SEQ_MGR_VS_GAME_FINAL_RESULT_SCORE_PTR = 0x294
|
|
35
|
+
SCORE_OBJECT_TEAM_RESULTS_ARY_PTR = 0x10
|
|
36
|
+
TEAM_RESULTS_ARY_SIZE_REGULAR = 0x18
|
|
37
|
+
TEAM_RESULTS_ARY_SIZE_RANKED = 0x2C
|
|
38
|
+
TEAM_RESULTS_PAINT_PC = 0x4
|
|
39
|
+
TEAM_RESULTS_WON = 0xC
|
|
40
|
+
TEAM_RESULTS_RANKED_OBJ_COUNT = 0x10
|
|
41
|
+
TEAM_RESULTS_POINTS = 0x14
|
|
42
|
+
PAGE_HANDLER_RESULTS_PAGE_PTR = 0x118
|
|
43
|
+
PAGE_HANDLER_LEADERBOARD_PAGE_PTR = 0x11C
|
|
44
|
+
RESULTS_PAGE_PHASE = 0x1E8
|
|
45
|
+
LEADERBOARD_PAGE_SLOTS = 0x214
|
|
46
|
+
LEADERBOARD_PAGE_ROWS_PTR = 0x218
|
|
47
|
+
ROWS_PLAYER_STRIDE = 0x20
|
|
48
|
+
ROWS_PLAYER_IDX = 0x0
|
|
49
|
+
ROWS_PLAYER_PLAYERINFO_PTR = 0x4
|
|
50
|
+
ROWS_PLAYER_TEAM = 0x8
|
|
51
|
+
ROWS_PLAYER_POINTS = 0xC
|
|
52
|
+
ROWS_PLAYER_KILLS = 0x10
|
|
53
|
+
ROWS_PLAYER_DEATHS = 0x14
|
|
54
|
+
ROWS_PLAYER_LOCAL_PLAYER_FLAG = 0x1C
|
|
55
|
+
FRAMEWORK_PEER_MGR_PTR = 0x14
|
|
56
|
+
PEER_MGR_ID_MGR_PTR = 0x84
|
|
57
|
+
ID_MGR_SLOT_ARY_PTR = 0x8
|
|
58
|
+
SLOT_SIZE = 0x14
|
|
59
|
+
SLOT_STATE = 0x0
|
|
60
|
+
SLOT_PID = 0x4
|
|
61
|
+
SLOT_PLAYER_ID = 0x12
|
|
62
|
+
STATICMEM_PLAYER_INFO_ARY_PTR = 0x10
|
|
63
|
+
STATICMEM_STAGE = 0x28
|
|
64
|
+
STATICMEM_MATCH_HOUR = 0x234
|
|
65
|
+
STATICMEM_VERSUS_MODE = 0x238
|
|
66
|
+
STATICMEM_VERSUS_RULE = 0x23C
|
|
67
|
+
PLAYER_INFO_NAME = 0x6
|
|
68
|
+
PLAYER_INFO_NAME_LENGTH = 0x20
|
|
69
|
+
PLAYER_INFO_REGION = 0x2C
|
|
70
|
+
PLAYER_INFO_TEAM = 0x33
|
|
71
|
+
PLAYER_INFO_GENDER = 0x37
|
|
72
|
+
PLAYER_INFO_SKIN_TONE = 0x3B
|
|
73
|
+
PLAYER_INFO_EYE_COLOR = 0x3F
|
|
74
|
+
PLAYER_INFO_WEAPON_SET = 0x42
|
|
75
|
+
PLAYER_INFO_MAIN_WEAPON = 0x46
|
|
76
|
+
PLAYER_INFO_SUB_WEAPON = 0x4A
|
|
77
|
+
PLAYER_INFO_SPECIAL_WEAPON = 0x4F
|
|
78
|
+
PLAYER_INFO_SHOES = 0x54
|
|
79
|
+
PLAYER_INFO_CLOTHES = 0x70
|
|
80
|
+
PLAYER_INFO_HEADGEAR = 0x8C
|
|
81
|
+
PLAYER_INFO_INK_TANK = 0xAA
|
|
82
|
+
PLAYER_INFO_LEVEL = 0xAF
|
|
83
|
+
PLAYER_INFO_RANK = 0xB3
|
|
84
|
+
PLAYER_INFO_FEST_TEAM = 0xB7
|
|
85
|
+
PLAYER_INFO_FEST_ID = 0xBA
|
|
86
|
+
PLAYER_INFO_FEST_RANK = 0xBF
|
|
87
|
+
PLAYER_INFO_PID = 0xD0
|
|
88
|
+
PLAYER_INFO_FEST_POWER = 0xD6
|
|
89
|
+
PLAYER_INFO_SIZE = 0xD8
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class OffsetChains(Enum):
|
|
93
|
+
ID_MANAGER = (
|
|
94
|
+
Offsets.FRAMEWORK_PEER_MGR_PTR,
|
|
95
|
+
Offsets.PEER_MGR_ID_MGR_PTR,
|
|
96
|
+
0
|
|
97
|
+
)
|
|
98
|
+
ID_MANAGER_SLOT_ARRAY = (
|
|
99
|
+
Offsets.ID_MGR_SLOT_ARY_PTR,
|
|
100
|
+
0
|
|
101
|
+
)
|
|
102
|
+
RESULTS_PHASE_ADDRESS = (
|
|
103
|
+
Offsets.MAINMGR_VS_GAME_PAGE_HANDLER_PTR,
|
|
104
|
+
Offsets.PAGE_HANDLER_RESULTS_PAGE_PTR,
|
|
105
|
+
Offsets.RESULTS_PAGE_PHASE
|
|
106
|
+
)
|
|
107
|
+
LEADERBOARD_PAGE = (
|
|
108
|
+
Offsets.MAINMGR_VS_GAME_PAGE_HANDLER_PTR,
|
|
109
|
+
Offsets.PAGE_HANDLER_LEADERBOARD_PAGE_PTR,
|
|
110
|
+
0
|
|
111
|
+
)
|
|
112
|
+
TEAM_RESULTS_ARRAY = (
|
|
113
|
+
Offsets.MAINMGR_VS_GAME_SEQ_MGR_VS_GAME_PTR,
|
|
114
|
+
Offsets.SEQ_MGR_VS_GAME_FINAL_RESULT_PTR,
|
|
115
|
+
Offsets.SEQ_MGR_VS_GAME_FINAL_RESULT_SCORE_PTR,
|
|
116
|
+
Offsets.SCORE_OBJECT_TEAM_RESULTS_ARY_PTR,
|
|
117
|
+
0
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def offsets(self) -> list[int]:
|
|
122
|
+
return list(self.value)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class VSMode(IntEnum):
|
|
126
|
+
REGULAR = 0
|
|
127
|
+
GACHI = 1
|
|
128
|
+
FEST = 2
|
|
129
|
+
PRIVATE = 3
|
|
130
|
+
PARTY = 4
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class VSRule(IntEnum):
|
|
134
|
+
TURF = 0
|
|
135
|
+
RAINMAKER = 1
|
|
136
|
+
ZONES = 2
|
|
137
|
+
TOWER = 3
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# Splatlogger (c) 2025 Shadow Doggo.
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
-
import platform
|
|
5
4
|
import sys
|
|
6
5
|
import time
|
|
7
6
|
from datetime import datetime
|
|
@@ -12,8 +11,8 @@ from typing import Any
|
|
|
12
11
|
import userpaths # type: ignore[import-untyped]
|
|
13
12
|
|
|
14
13
|
from . import utility_functions as uf
|
|
15
|
-
from .enums import
|
|
16
|
-
from .tuples import AccountInfo, AppContext, CommonAddresses, PlayerInfo
|
|
14
|
+
from .enums import OffsetChains, Offsets, Pointers, VSMode, VSRule
|
|
15
|
+
from .tuples import AccountInfo, AppContext, CommonAddresses, PlayerInfo, PlayerStats, MatchResults
|
|
17
16
|
|
|
18
17
|
|
|
19
18
|
def get_resource_path(relative_path: str | Path) -> Path:
|
|
@@ -76,27 +75,38 @@ class MatchLogger:
|
|
|
76
75
|
match_info = self._get_match_info()
|
|
77
76
|
versus_mode, versus_rule = match_info[2], match_info[3]
|
|
78
77
|
if self._ctx.options.log_level == "standard":
|
|
79
|
-
match_log = (
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
match_log = (
|
|
79
|
+
f"\nMatch {match_count}\n\n{uf.player_table_header(self._ctx.options.service == 'altendo')}\n"
|
|
80
|
+
if self._ctx.options.auto_logging or self._ctx.options.single_log
|
|
81
|
+
else f"\n{uf.player_table_header(self._ctx.options.service == 'altendo')}\n"
|
|
82
|
+
)
|
|
82
83
|
else:
|
|
83
84
|
match_log = self._format_match_info(match_count, date, session_id, match_info)
|
|
84
85
|
|
|
85
|
-
dcd_players:
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
86
|
+
dcd_players: set[int] = set()
|
|
87
|
+
stats: list[PlayerStats] = []
|
|
88
|
+
match_results: MatchResults | None = None
|
|
89
|
+
if self._ctx.options.log_level == "full":
|
|
90
|
+
if self._wait_for_results(adrs, dcd_players):
|
|
91
|
+
stats = self._get_player_stats()
|
|
92
|
+
match_results = self._get_match_results(versus_rule)
|
|
89
93
|
else:
|
|
90
94
|
disconnect = True
|
|
91
95
|
|
|
96
|
+
stats_by_idx = {row.idx: row for row in stats}
|
|
97
|
+
|
|
92
98
|
for player_info in player_info_list:
|
|
93
99
|
match_log += self._format_match_log(player_info, versus_mode)
|
|
94
100
|
|
|
95
|
-
if self._ctx.options.log_level == "
|
|
96
|
-
if
|
|
97
|
-
match_log +=
|
|
98
|
-
|
|
99
|
-
|
|
101
|
+
if self._ctx.options.log_level == "full" and not disconnect:
|
|
102
|
+
if (player_stats := stats_by_idx.get(player_info.idx)) is not None:
|
|
103
|
+
match_log += self._format_stats_log(
|
|
104
|
+
player_info, match_results.winning_team if match_results else None,
|
|
105
|
+
versus_rule, player_stats, dcd_players
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
if match_results:
|
|
109
|
+
match_log += self._format_match_results(match_results, versus_rule)
|
|
100
110
|
|
|
101
111
|
if self._ctx.options.log_level == "standard":
|
|
102
112
|
match_log += (
|
|
@@ -120,12 +130,12 @@ class MatchLogger:
|
|
|
120
130
|
|
|
121
131
|
def _get_match_info(self) -> tuple[str, int, int, int]:
|
|
122
132
|
return (
|
|
123
|
-
self._ctx.gecko.read_string(self._ctx.static_mem_adr + Offsets.STATICMEM_STAGE, length=32)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
self._ctx.gecko.peek8(self._ctx.static_mem_adr + Offsets.STATICMEM_VERSUS_MODE + 0x3),
|
|
128
|
-
self._ctx.gecko.peek8(self._ctx.static_mem_adr + Offsets.STATICMEM_VERSUS_RULE + 0x3)
|
|
133
|
+
self._ctx.gecko.read_string(self._ctx.static_mem_adr + Offsets.STATICMEM_STAGE, length=32)
|
|
134
|
+
.split("\u0000")[0],
|
|
135
|
+
# Read last byte as sint8
|
|
136
|
+
self._ctx.gecko.peek8(self._ctx.static_mem_adr + Offsets.STATICMEM_MATCH_HOUR + 0x3, signed=True),
|
|
137
|
+
self._ctx.gecko.peek8(self._ctx.static_mem_adr + Offsets.STATICMEM_VERSUS_MODE + 0x3, signed=True),
|
|
138
|
+
self._ctx.gecko.peek8(self._ctx.static_mem_adr + Offsets.STATICMEM_VERSUS_RULE + 0x3, signed=True)
|
|
129
139
|
)
|
|
130
140
|
|
|
131
141
|
def _format_match_info(self, match_count: int, date: datetime, session_id: int,
|
|
@@ -160,7 +170,7 @@ class MatchLogger:
|
|
|
160
170
|
)
|
|
161
171
|
|
|
162
172
|
return (
|
|
163
|
-
|
|
173
|
+
uf.player_table_entry(player_info, AccountInfo(player_info.network_id, player_info.mii_name, "")) + "\n"
|
|
164
174
|
) if self._ctx.options.log_level == "standard" else (
|
|
165
175
|
f"\n{' ' * self._align}[Player {player_info.idx + 1}]\n"
|
|
166
176
|
f"{indent}Name: {player_info.name}"
|
|
@@ -191,83 +201,144 @@ class MatchLogger:
|
|
|
191
201
|
f"{fest_info if vs_mode == VSMode.FEST else ''}"
|
|
192
202
|
)
|
|
193
203
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
def
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
win_team_adr = stack_adr + Offsets.CEMU_WIN_TEAM
|
|
229
|
-
|
|
230
|
-
return stats_adr, win_team_adr
|
|
231
|
-
|
|
232
|
-
def _wait_for_results(self, adrs: CommonAddresses) -> bool:
|
|
233
|
-
for _ in range(200):
|
|
204
|
+
def _format_match_results(self, match_results: MatchResults, versus_rule: int) -> str:
|
|
205
|
+
indent = ' ' * (self._align + 2)
|
|
206
|
+
|
|
207
|
+
if versus_rule in (VSRule.RAINMAKER, VSRule.ZONES, VSRule.TOWER):
|
|
208
|
+
return (
|
|
209
|
+
f"\n{' ' * self._align}[Match Results]\n"
|
|
210
|
+
f"{indent}Winner: {'Bravo' if match_results.winning_team == 1 else 'Alpha'}\n"
|
|
211
|
+
f"{indent}Alpha points: {match_results.alpha_points}\n"
|
|
212
|
+
f"{indent}Bravo points: {match_results.bravo_points}\n"
|
|
213
|
+
f"{indent}Alpha score: {100 - match_results.alpha_count}\n"
|
|
214
|
+
f"{indent}Bravo score: {100 - match_results.bravo_count}\n"
|
|
215
|
+
)
|
|
216
|
+
else:
|
|
217
|
+
return (
|
|
218
|
+
f"\n{' ' * self._align}[Match Results]\n"
|
|
219
|
+
f"{indent}Winner: {'Bravo' if match_results.winning_team == 1 else 'Alpha'}\n"
|
|
220
|
+
f"{indent}Alpha painted: {match_results.alpha_paint_pc / 10:.1f}%\n"
|
|
221
|
+
f"{indent}Bravo painted: {match_results.bravo_paint_pc / 10:.1f}%\n"
|
|
222
|
+
f"{indent}Alpha points: {match_results.alpha_points}\n"
|
|
223
|
+
f"{indent}Bravo points: {match_results.bravo_points}\n"
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
def _wait_for_results(self, adrs: CommonAddresses, dcd_players: set[int]) -> bool:
|
|
227
|
+
id_mgr_adr = self._ctx.gecko.resolve_pointer_chain(
|
|
228
|
+
Pointers.FRAMEWORK.convert(self._ctx.options.platform),
|
|
229
|
+
OffsetChains.ID_MANAGER.offsets
|
|
230
|
+
)
|
|
231
|
+
slots_count = 0
|
|
232
|
+
slot_ary_adr = 0
|
|
233
|
+
if id_mgr_adr != 0:
|
|
234
|
+
slots_count = self._ctx.gecko.peek32(id_mgr_adr)
|
|
235
|
+
slot_ary_adr = self._ctx.gecko.peek32(id_mgr_adr + Offsets.ID_MGR_SLOT_ARY_PTR)
|
|
236
|
+
|
|
237
|
+
for _ in range(300):
|
|
234
238
|
if not self._is_player_in_vs_match():
|
|
235
239
|
return False
|
|
236
240
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
241
|
+
phase_adr = self._ctx.gecko.resolve_pointer_chain(
|
|
242
|
+
Pointers.MAIN_MGR_VS_GAME.convert(self._ctx.options.platform),
|
|
243
|
+
OffsetChains.RESULTS_PHASE_ADDRESS.offsets
|
|
244
|
+
)
|
|
240
245
|
|
|
241
|
-
|
|
246
|
+
if phase_adr != 0:
|
|
247
|
+
phase = self._ctx.gecko.peek8(phase_adr)
|
|
248
|
+
if 2 <= phase <= 14: # Judging/results states post fade-in.
|
|
249
|
+
return True
|
|
250
|
+
|
|
251
|
+
dcd_players.update(self._check_for_dcs(slots_count, slot_ary_adr))
|
|
252
|
+
|
|
253
|
+
time.sleep(2)
|
|
242
254
|
|
|
243
255
|
return False
|
|
244
256
|
|
|
245
|
-
def
|
|
246
|
-
|
|
247
|
-
|
|
257
|
+
def _get_player_stats(self) -> list[PlayerStats]:
|
|
258
|
+
leaderboard_page_adr = self._ctx.gecko.resolve_pointer_chain(
|
|
259
|
+
Pointers.MAIN_MGR_VS_GAME.convert(self._ctx.options.platform),
|
|
260
|
+
OffsetChains.LEADERBOARD_PAGE.offsets
|
|
261
|
+
)
|
|
262
|
+
if leaderboard_page_adr == 0:
|
|
263
|
+
return []
|
|
264
|
+
|
|
265
|
+
leaderboard_slots_num = self._ctx.gecko.peek32(leaderboard_page_adr + Offsets.LEADERBOARD_PAGE_SLOTS)
|
|
266
|
+
leaderboard_rows_adr = self._ctx.gecko.peek32(leaderboard_page_adr + Offsets.LEADERBOARD_PAGE_ROWS_PTR)
|
|
248
267
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
deaths = uf.read_int(stats, offset + Offsets.STATS_DEATHS, 2)
|
|
268
|
+
if not 1 <= leaderboard_slots_num <= 8 or leaderboard_rows_adr == 0:
|
|
269
|
+
return []
|
|
252
270
|
|
|
253
|
-
|
|
271
|
+
size = leaderboard_slots_num * Offsets.ROWS_PLAYER_STRIDE
|
|
272
|
+
stats = self._ctx.gecko.peek_raw(leaderboard_rows_adr, length=size)
|
|
273
|
+
slots = []
|
|
274
|
+
for slot in range(leaderboard_slots_num):
|
|
275
|
+
offset = slot * Offsets.ROWS_PLAYER_STRIDE
|
|
276
|
+
idx = uf.read_int(stats, offset + Offsets.ROWS_PLAYER_IDX, 4, signed=True)
|
|
277
|
+
team = uf.read_int(stats, offset + Offsets.ROWS_PLAYER_TEAM, 4, signed=True)
|
|
278
|
+
if not 0 <= idx < 8 or team not in (0, 1): continue
|
|
279
|
+
points = uf.read_int(stats, offset + Offsets.ROWS_PLAYER_POINTS, 4)
|
|
280
|
+
kills = uf.read_int(stats, offset + Offsets.ROWS_PLAYER_KILLS, 4)
|
|
281
|
+
deaths = uf.read_int(stats, offset + Offsets.ROWS_PLAYER_DEATHS, 4)
|
|
282
|
+
local_player = uf.read_int(stats, offset + Offsets.ROWS_PLAYER_LOCAL_PLAYER_FLAG, 1) == 1
|
|
283
|
+
|
|
284
|
+
slots.append(PlayerStats(idx, team, points, kills, deaths, local_player))
|
|
285
|
+
|
|
286
|
+
return sorted(slots, key=lambda x: x.idx)
|
|
287
|
+
|
|
288
|
+
def _get_match_results(self, versus_rule: int) -> MatchResults | None:
|
|
289
|
+
team_results_ary_adr = self._ctx.gecko.resolve_pointer_chain(
|
|
290
|
+
Pointers.MAIN_MGR_VS_GAME.convert(self._ctx.options.platform),
|
|
291
|
+
OffsetChains.TEAM_RESULTS_ARRAY.offsets
|
|
292
|
+
)
|
|
293
|
+
if team_results_ary_adr == 0:
|
|
294
|
+
return None
|
|
295
|
+
|
|
296
|
+
if versus_rule in (VSRule.RAINMAKER, VSRule.ZONES, VSRule.TOWER):
|
|
297
|
+
alpha_results = self._ctx.gecko.peek_raw(team_results_ary_adr, Offsets.TEAM_RESULTS_ARY_SIZE_RANKED)
|
|
298
|
+
bravo_results = self._ctx.gecko.peek_raw(team_results_ary_adr + Offsets.TEAM_RESULTS_ARY_SIZE_RANKED,
|
|
299
|
+
Offsets.TEAM_RESULTS_ARY_SIZE_RANKED)
|
|
300
|
+
|
|
301
|
+
return MatchResults(
|
|
302
|
+
winning_team=uf.read_int(bravo_results, Offsets.TEAM_RESULTS_WON, 1),
|
|
303
|
+
alpha_paint_pc=uf.read_int(alpha_results, Offsets.TEAM_RESULTS_PAINT_PC, 4),
|
|
304
|
+
bravo_paint_pc=uf.read_int(bravo_results, Offsets.TEAM_RESULTS_PAINT_PC, 4),
|
|
305
|
+
alpha_points=uf.read_int(alpha_results, Offsets.TEAM_RESULTS_POINTS, 4),
|
|
306
|
+
bravo_points=uf.read_int(bravo_results, Offsets.TEAM_RESULTS_POINTS, 4),
|
|
307
|
+
alpha_count=uf.read_int(alpha_results, Offsets.TEAM_RESULTS_RANKED_OBJ_COUNT, 4),
|
|
308
|
+
bravo_count=uf.read_int(bravo_results, Offsets.TEAM_RESULTS_RANKED_OBJ_COUNT, 4)
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
alpha_results = self._ctx.gecko.peek_raw(team_results_ary_adr, Offsets.TEAM_RESULTS_ARY_SIZE_REGULAR)
|
|
312
|
+
bravo_results = self._ctx.gecko.peek_raw(team_results_ary_adr + Offsets.TEAM_RESULTS_ARY_SIZE_REGULAR,
|
|
313
|
+
Offsets.TEAM_RESULTS_ARY_SIZE_REGULAR)
|
|
314
|
+
|
|
315
|
+
return MatchResults(
|
|
316
|
+
winning_team=uf.read_int(bravo_results, Offsets.TEAM_RESULTS_WON, 1),
|
|
317
|
+
alpha_paint_pc=uf.read_int(alpha_results, Offsets.TEAM_RESULTS_PAINT_PC, 4),
|
|
318
|
+
bravo_paint_pc=uf.read_int(bravo_results, Offsets.TEAM_RESULTS_PAINT_PC, 4),
|
|
319
|
+
alpha_points=uf.read_int(alpha_results, Offsets.TEAM_RESULTS_POINTS, 4),
|
|
320
|
+
bravo_points=uf.read_int(bravo_results, Offsets.TEAM_RESULTS_POINTS, 4),
|
|
321
|
+
alpha_count=0,
|
|
322
|
+
bravo_count=0
|
|
323
|
+
)
|
|
254
324
|
|
|
255
|
-
def _format_stats_log(self, player_info: PlayerInfo, winning_team: int, versus_rule: int,
|
|
256
|
-
stats:
|
|
325
|
+
def _format_stats_log(self, player_info: PlayerInfo, winning_team: int | None, versus_rule: int,
|
|
326
|
+
stats: PlayerStats, dcd_players: set[int]) -> str:
|
|
257
327
|
indent = " " * (self._align + 2)
|
|
258
|
-
player_won = player_info.team == winning_team
|
|
328
|
+
player_won = player_info.team == winning_team if winning_team is not None else None
|
|
259
329
|
player_dcd = player_info.pid in dcd_players
|
|
260
|
-
points, kills, deaths = stats
|
|
261
330
|
|
|
262
331
|
points_log = ""
|
|
263
332
|
if versus_rule == VSRule.TURF: # Only log points in turf war cause in ranked they're the same for all players.
|
|
264
333
|
if player_won:
|
|
265
|
-
points_log = f"{indent}Points: {points
|
|
334
|
+
points_log = f"{indent}Points: {stats.points}p ({stats.points - 1000}p w/o win bonus)\n"
|
|
266
335
|
else:
|
|
267
|
-
points_log = f"{indent}Points: {points}p\n"
|
|
336
|
+
points_log = f"{indent}Points: {stats.points}p\n"
|
|
268
337
|
|
|
269
338
|
if player_dcd:
|
|
270
339
|
result = "Disconnected"
|
|
340
|
+
elif player_won is None:
|
|
341
|
+
result = "Unknown"
|
|
271
342
|
elif player_won:
|
|
272
343
|
result = "Win"
|
|
273
344
|
else:
|
|
@@ -275,47 +346,26 @@ class MatchLogger:
|
|
|
275
346
|
|
|
276
347
|
return (
|
|
277
348
|
f"{points_log}"
|
|
278
|
-
f"{indent}K/D: {kills}/{deaths}\n"
|
|
349
|
+
f"{indent}K/D: {stats.kills}/{stats.deaths}\n"
|
|
279
350
|
f"{indent}Result: {result}\n"
|
|
280
351
|
)
|
|
281
352
|
|
|
282
|
-
def
|
|
283
|
-
|
|
284
|
-
first_player_info_adr = self._ctx.gecko.peek32(player_info_ary_adr)
|
|
285
|
-
|
|
286
|
-
# Check default offsets of 0 and -0x38 first. The pointer is at 0x30.
|
|
287
|
-
if self._ctx.gecko.peek32(Addresses.STATS + 0x30) == first_player_info_adr:
|
|
288
|
-
return 0
|
|
289
|
-
if self._ctx.gecko.peek32(Addresses.STATS - 0x8) == first_player_info_adr:
|
|
290
|
-
return -0x38
|
|
291
|
-
|
|
292
|
-
search_space = self._ctx.gecko.peek_raw(Addresses.STATS - 0x100, length=0x200)
|
|
293
|
-
stats_offset = search_space.find(first_player_info_adr.to_bytes(length=4, byteorder="big"))
|
|
294
|
-
|
|
295
|
-
return None if stats_offset == -1 else stats_offset - 0x130
|
|
296
|
-
|
|
297
|
-
def _check_for_dcs(self, session_id: int) -> list[int]:
|
|
298
|
-
# Magic numbers here cause I have no idea what class/struct this belongs to,
|
|
299
|
-
# I just happened to find a pointer chain to it.
|
|
300
|
-
lobby_history_adr = self._ctx.gecko.resolve_pointer_chain(0x106EB0A0, [0xC, 0x78, 0x10])
|
|
301
|
-
if lobby_history_adr == 0:
|
|
302
|
-
return []
|
|
303
|
-
|
|
304
|
-
lobby_history = self._ctx.gecko.peek_raw(lobby_history_adr, length=0x1D0)
|
|
353
|
+
def _check_for_dcs(self, slots_count: int, slot_ary_adr: int) -> set[int]:
|
|
354
|
+
dcd_players: set[int] = set()
|
|
305
355
|
|
|
306
|
-
|
|
356
|
+
for offset in range(0, slots_count * 4, 4):
|
|
357
|
+
slot_adr = self._ctx.gecko.peek32(slot_ary_adr + offset)
|
|
358
|
+
if slot_adr == 0: continue
|
|
359
|
+
slot = self._ctx.gecko.peek_raw(slot_adr, Offsets.SLOT_SIZE)
|
|
307
360
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
status = uf.read_int(lobby_history, 0x4 + offset, 4)
|
|
312
|
-
player_id = uf.read_int(lobby_history, 0xC + offset, 4)
|
|
313
|
-
latest_player_status[player_id] = status
|
|
361
|
+
state = uf.read_int(slot, Offsets.SLOT_STATE, 4)
|
|
362
|
+
pid = uf.read_int(slot, Offsets.SLOT_PID, 4)
|
|
363
|
+
player_id = uf.read_int(slot, Offsets.SLOT_PLAYER_ID, 1)
|
|
314
364
|
|
|
315
|
-
|
|
365
|
+
if state == 3 and player_id != 0xFF:
|
|
366
|
+
dcd_players.add(pid)
|
|
316
367
|
|
|
317
|
-
|
|
318
|
-
return [player_id for player_id, status in latest_player_status.items() if status == 0x7 or status == 0x8]
|
|
368
|
+
return dcd_players
|
|
319
369
|
|
|
320
370
|
def _is_player_in_vs_match(self) -> bool:
|
|
321
|
-
return self._ctx.gecko.peek32(
|
|
371
|
+
return self._ctx.gecko.peek32(Pointers.MAIN_MGR_VS_GAME.convert(self._ctx.options.platform)) != 0
|
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
},
|
|
6
6
|
|
|
7
7
|
"versus_mode_name": {
|
|
8
|
+
"-1": "?cNone",
|
|
8
9
|
"0": "Regular Battle",
|
|
9
10
|
"1": "Ranked Battle",
|
|
10
11
|
"2": "Splatfest Battle",
|
|
11
12
|
"3": "Private Battle",
|
|
12
|
-
"4": "Squad Battle"
|
|
13
|
-
"255": "None"
|
|
13
|
+
"4": "Squad Battle"
|
|
14
14
|
},
|
|
15
15
|
|
|
16
16
|
"versus_rule_name": {
|
|
@@ -18,8 +18,7 @@
|
|
|
18
18
|
"0": "Turf War",
|
|
19
19
|
"1": "Rainmaker",
|
|
20
20
|
"2": "Splat Zones",
|
|
21
|
-
"3": "Tower Control"
|
|
22
|
-
"255": "None"
|
|
21
|
+
"3": "Tower Control"
|
|
23
22
|
},
|
|
24
23
|
|
|
25
24
|
"team_name": {
|
|
@@ -17,10 +17,10 @@ import userpaths # type: ignore[import-untyped]
|
|
|
17
17
|
|
|
18
18
|
from . import utility_functions as uf
|
|
19
19
|
from .enums import Offsets, Pointers
|
|
20
|
+
from .errors import SplatloggerError
|
|
20
21
|
from .match_logger import MatchLogger
|
|
21
22
|
from .tcpgecko import GeckoConnectionFailedError, TCPGecko
|
|
22
23
|
from .tuples import AccountInfo, AppContext, CommonAddresses, Options, PlayerInfo
|
|
23
|
-
from .errors import SplatloggerError
|
|
24
24
|
|
|
25
25
|
if TYPE_CHECKING:
|
|
26
26
|
from .cemu_gecko import CemuGecko
|
|
@@ -90,7 +90,8 @@ def command_lobby_info(ctx: AppContext) -> bool:
|
|
|
90
90
|
|
|
91
91
|
# Not reading through names.json just for this.
|
|
92
92
|
vs_modes = (
|
|
93
|
-
"Regular Battle", "Ranked Battle", "Splatfest Battle", "Private Battle", "Squad Battle"
|
|
93
|
+
"Regular Battle", "Ranked Battle", "Splatfest Battle", "Private Battle", "Squad Battle"
|
|
94
|
+
)
|
|
94
95
|
vs_mode = ctx.gecko.peek8(ctx.static_mem_adr + Offsets.STATICMEM_VERSUS_MODE + 0x3)
|
|
95
96
|
print(f"Lobby type: {vs_modes[vs_mode] if 0 <= vs_mode < len(vs_modes) else 'Unknown'}")
|
|
96
97
|
|
|
@@ -101,7 +102,7 @@ def command_lobby_info(ctx: AppContext) -> bool:
|
|
|
101
102
|
f"\n{'PID':<10} | {'ANID' if ctx.options.service == 'altendo' else 'PNID':<16}\n{'-' * 30}"
|
|
102
103
|
)
|
|
103
104
|
for player in players:
|
|
104
|
-
print(f"{player[0]} | {player[1]}")
|
|
105
|
+
print(f"{player[0]:<10} | {player[1]}")
|
|
105
106
|
else:
|
|
106
107
|
print("\nNo active session.")
|
|
107
108
|
finally:
|
|
@@ -156,7 +157,7 @@ def create_tcpgecko_instance(options: Options) -> TCPGecko | CemuGecko:
|
|
|
156
157
|
|
|
157
158
|
@uf.handle_gecko_errors
|
|
158
159
|
def get_static_mem_adr(gecko: TCPGecko | CemuGecko, options: Options) -> int:
|
|
159
|
-
static_mem_adr = gecko.peek32(
|
|
160
|
+
static_mem_adr = gecko.peek32(Pointers.STATIC_MEM.convert(options.platform))
|
|
160
161
|
if static_mem_adr == 0:
|
|
161
162
|
msg = (
|
|
162
163
|
"Successfully opened Cemu process but could not read StaticMem address. "
|
|
@@ -229,13 +230,13 @@ def splatlog(ctx: AppContext, match_logger: MatchLogger | None, match_count: int
|
|
|
229
230
|
|
|
230
231
|
@uf.handle_gecko_errors
|
|
231
232
|
def get_common_addresses(ctx: AppContext) -> CommonAddresses:
|
|
232
|
-
return CommonAddresses(ctx.gecko.peek32(
|
|
233
|
-
ctx.gecko.peek32(ctx.static_mem_adr + Offsets.
|
|
233
|
+
return CommonAddresses(ctx.gecko.peek32(Pointers.MAIN_MGR_BASE.convert(ctx.options.platform)),
|
|
234
|
+
ctx.gecko.peek32(ctx.static_mem_adr + Offsets.STATICMEM_PLAYER_INFO_ARY_PTR))
|
|
234
235
|
|
|
235
236
|
|
|
236
237
|
@uf.handle_gecko_errors
|
|
237
238
|
def get_session_id(ctx: AppContext) -> int:
|
|
238
|
-
if (session_adr := ctx.gecko.peek32(
|
|
239
|
+
if (session_adr := ctx.gecko.peek32(Pointers.SESSION.convert(ctx.options.platform))) != 0:
|
|
239
240
|
session_id_idx = int.from_bytes(ctx.gecko.peek_raw(session_adr + Offsets.SESSION_ID_IDX,
|
|
240
241
|
length=1), byteorder="big")
|
|
241
242
|
return ctx.gecko.peek32(session_adr + session_id_idx * 4 + Offsets.SESSION_ID)
|
|
@@ -246,10 +247,14 @@ def get_session_id(ctx: AppContext) -> int:
|
|
|
246
247
|
@uf.handle_gecko_errors
|
|
247
248
|
def get_player_count(ctx: AppContext, main_mgr_base_adr: int) -> int:
|
|
248
249
|
if is_player_in_vs_match(ctx) or ctx.options.auto_logging:
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
250
|
+
player_count_adr = ctx.gecko.resolve_pointer_chain(
|
|
251
|
+
main_mgr_base_adr + Offsets.MAINMGR_PLAYER_MGR_PTR,
|
|
252
|
+
[Offsets.PLAYERMGR_PLAYER_COUNT]
|
|
253
|
+
)
|
|
254
|
+
if player_count_adr != 0:
|
|
255
|
+
player_count = ctx.gecko.peek32(player_count_adr)
|
|
256
|
+
if 1 <= player_count <= 8:
|
|
257
|
+
return player_count
|
|
253
258
|
else:
|
|
254
259
|
uf.print_warning("You are currently not in a match. Logging previous match data "
|
|
255
260
|
"(Player 1 data will be your own, Session ID will be the current one).\n")
|
|
@@ -259,19 +264,19 @@ def get_player_count(ctx: AppContext, main_mgr_base_adr: int) -> int:
|
|
|
259
264
|
|
|
260
265
|
@uf.handle_gecko_errors
|
|
261
266
|
def is_player_in_vs_match(ctx: AppContext) -> bool:
|
|
262
|
-
return ctx.gecko.peek32(
|
|
267
|
+
return ctx.gecko.peek32(Pointers.MAIN_MGR_VS_GAME.convert(ctx.options.platform)) != 0
|
|
263
268
|
|
|
264
269
|
|
|
265
|
-
def collect_player_info(ctx: AppContext, player_count: int,
|
|
266
|
-
|
|
270
|
+
def collect_player_info(ctx: AppContext, player_count: int,
|
|
271
|
+
player_info_ary_adr: int) -> Iterator[tuple[PlayerInfo, AccountInfo]]:
|
|
267
272
|
for player_idx in range(player_count):
|
|
268
273
|
if player_info_tuple := get_player_info(ctx, player_idx, player_info_ary_adr):
|
|
269
274
|
yield player_info_tuple
|
|
270
275
|
|
|
271
276
|
|
|
272
277
|
@uf.handle_gecko_errors
|
|
273
|
-
def get_player_info(ctx: AppContext, player_idx: int,
|
|
274
|
-
|
|
278
|
+
def get_player_info(ctx: AppContext, player_idx: int,
|
|
279
|
+
player_info_ary_adr: int) -> tuple[PlayerInfo, AccountInfo] | None:
|
|
275
280
|
player_info_adr = ctx.gecko.peek32(player_info_ary_adr + player_idx * 0x4)
|
|
276
281
|
if player_info_adr == 0:
|
|
277
282
|
return None
|
|
@@ -290,17 +295,22 @@ def get_player_info(ctx: AppContext, player_idx: int, player_info_ary_adr: int)
|
|
|
290
295
|
|
|
291
296
|
|
|
292
297
|
def decode_player_name(player_info_raw: bytes) -> str:
|
|
293
|
-
return clean(
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
298
|
+
return clean(
|
|
299
|
+
player_info_raw[Offsets.PLAYER_INFO_NAME:Offsets.PLAYER_INFO_NAME + Offsets.PLAYER_INFO_NAME_LENGTH]
|
|
300
|
+
.decode("utf-16-be", errors="replace")
|
|
301
|
+
.split("\u0000")[0],
|
|
302
|
+
to_ascii=False,
|
|
303
|
+
lower=False,
|
|
304
|
+
no_line_breaks=True
|
|
305
|
+
)
|
|
297
306
|
|
|
298
307
|
|
|
299
308
|
def get_player_account_info(ctx: AppContext, player_pid: int, player_name: str) -> AccountInfo:
|
|
300
309
|
if ctx.options.service == "none":
|
|
301
310
|
return AccountInfo("", player_name, "")
|
|
302
311
|
|
|
303
|
-
act_info = get_altendo_account_info(player_pid) if ctx.options.service == "altendo" else get_pn_account_info(
|
|
312
|
+
act_info = get_altendo_account_info(player_pid) if ctx.options.service == "altendo" else get_pn_account_info(
|
|
313
|
+
player_pid)
|
|
304
314
|
if act_info.request_error:
|
|
305
315
|
return AccountInfo("", player_name, act_info.request_error)
|
|
306
316
|
|
|
@@ -316,7 +326,7 @@ def build_player_info(player_idx: int, player_pid: int, player_name: str, act_in
|
|
|
316
326
|
name=player_name,
|
|
317
327
|
mii_name=act_info.mii_name,
|
|
318
328
|
region=uf.read_int(player_info_raw, Offsets.PLAYER_INFO_REGION, 4),
|
|
319
|
-
team=uf.read_int(player_info_raw, Offsets.PLAYER_INFO_TEAM, 1),
|
|
329
|
+
team=uf.read_int(player_info_raw, Offsets.PLAYER_INFO_TEAM, 1, signed=True),
|
|
320
330
|
gender=uf.read_int(player_info_raw, Offsets.PLAYER_INFO_GENDER, 1),
|
|
321
331
|
skin_tone=uf.read_int(player_info_raw, Offsets.PLAYER_INFO_SKIN_TONE, 1),
|
|
322
332
|
eye_color=uf.read_int(player_info_raw, Offsets.PLAYER_INFO_EYE_COLOR, 1),
|
|
@@ -339,32 +349,40 @@ def build_player_info(player_idx: int, player_pid: int, player_name: str, act_in
|
|
|
339
349
|
|
|
340
350
|
@uf.handle_gecko_errors
|
|
341
351
|
def get_lobby_players(ctx: AppContext) -> list[tuple[int, str]] | None:
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
[Offsets.SESSION_LOBBY, Offsets.LOBBY_PLAYER_LIST_HEAD])
|
|
345
|
-
if head_node_adr == 0:
|
|
352
|
+
session_adr = ctx.gecko.peek32(Pointers.SESSION.convert(ctx.options.platform))
|
|
353
|
+
if session_adr == 0:
|
|
346
354
|
return None
|
|
347
355
|
|
|
348
|
-
#
|
|
349
|
-
|
|
356
|
+
# Circular doubly linked list with a sentinel embedded in Session.
|
|
357
|
+
sentinel_adr = session_adr + Offsets.SESSION_ACTIVE_STATION_LIST_SENTINEL
|
|
358
|
+
header = ctx.gecko.peek_raw(sentinel_adr, 0xC)
|
|
359
|
+
current_node = uf.read_int(header, Offsets.STATION_LIST_NODE_NEXT_PTR, 4)
|
|
360
|
+
count = uf.read_int(header,
|
|
361
|
+
Offsets.SESSION_ACTIVE_STATION_LIST_COUNT - Offsets.SESSION_ACTIVE_STATION_LIST_SENTINEL, 4)
|
|
362
|
+
capacity = ctx.gecko.peek32(session_adr + Offsets.SESSION_STATION_NODE_CAPACITY)
|
|
363
|
+
|
|
364
|
+
if not 0 <= count <= capacity <= 10:
|
|
365
|
+
return None
|
|
350
366
|
|
|
351
|
-
|
|
367
|
+
player_ids = []
|
|
368
|
+
previous_node = sentinel_adr
|
|
352
369
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
node_data = ctx.gecko.peek_raw(current_node + 0x4, 8)
|
|
370
|
+
for _ in range(count):
|
|
371
|
+
if current_node == 0 or current_node == sentinel_adr:
|
|
372
|
+
return None
|
|
357
373
|
|
|
358
|
-
|
|
359
|
-
|
|
374
|
+
node = ctx.gecko.peek_raw(current_node, Offsets.STATION_LIST_NODE_SIZE)
|
|
375
|
+
if uf.read_int(node, Offsets.STATION_LIST_NODE_PREV_PTR, 4) != previous_node:
|
|
376
|
+
return None
|
|
360
377
|
|
|
378
|
+
player_id = uf.read_int(node, Offsets.STATION_LIST_NODE_STATION_ID, 4)
|
|
361
379
|
if player_id != 0:
|
|
362
|
-
|
|
380
|
+
player_ids.append(player_id)
|
|
363
381
|
|
|
364
|
-
|
|
365
|
-
|
|
382
|
+
previous_node = current_node
|
|
383
|
+
current_node = uf.read_int(node, Offsets.STATION_LIST_NODE_NEXT_PTR, 4)
|
|
366
384
|
|
|
367
|
-
return
|
|
385
|
+
return [(pid, get_player_account_info(ctx, pid, "").network_id) for pid in player_ids]
|
|
368
386
|
|
|
369
387
|
|
|
370
388
|
def get_pn_account_info(player_pid: int) -> AccountInfo:
|
|
@@ -22,7 +22,6 @@ class Options(NamedTuple):
|
|
|
22
22
|
service: str
|
|
23
23
|
single_log: bool
|
|
24
24
|
silent_logging: bool
|
|
25
|
-
stack: int | None
|
|
26
25
|
|
|
27
26
|
|
|
28
27
|
@dataclass
|
|
@@ -72,3 +71,22 @@ class PlayerInfo(NamedTuple):
|
|
|
72
71
|
fest_id: int
|
|
73
72
|
fest_rank: int
|
|
74
73
|
fest_power: int
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class PlayerStats(NamedTuple):
|
|
77
|
+
idx: int
|
|
78
|
+
team: int
|
|
79
|
+
points: int
|
|
80
|
+
kills: int
|
|
81
|
+
deaths: int
|
|
82
|
+
local_player: bool
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class MatchResults(NamedTuple):
|
|
86
|
+
winning_team: int
|
|
87
|
+
alpha_paint_pc: int
|
|
88
|
+
bravo_paint_pc: int
|
|
89
|
+
alpha_points: int
|
|
90
|
+
bravo_points: int
|
|
91
|
+
alpha_count: int
|
|
92
|
+
bravo_count: int
|
|
@@ -13,7 +13,6 @@ from xml.etree import ElementTree
|
|
|
13
13
|
import requests
|
|
14
14
|
from colorama import Fore, Style
|
|
15
15
|
|
|
16
|
-
from .enums import Offsets
|
|
17
16
|
from .errors import SplatloggerError
|
|
18
17
|
from .tcpgecko import GeckoConnectionClosedError, GeckoReadMemError, TCPGeckoError
|
|
19
18
|
from .tuples import AccountInfo, PlayerInfo
|
|
@@ -32,21 +31,19 @@ def player_table_header(altendo: bool) -> str:
|
|
|
32
31
|
|
|
33
32
|
|
|
34
33
|
def player_table_entry(player_info: PlayerInfo, act_info: AccountInfo) -> str:
|
|
35
|
-
return (
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
return (
|
|
35
|
+
f"Player {player_info.idx + 1} | "
|
|
36
|
+
f"{player_info.pid:<10} | "
|
|
37
|
+
f"{act_info.network_id:<16} | "
|
|
38
|
+
f"{player_info.name}"
|
|
39
|
+
f"{f' ({act_info.mii_name})' if not compare_names(player_info.name, act_info.mii_name) else ''}"
|
|
40
|
+
)
|
|
40
41
|
|
|
41
42
|
|
|
42
43
|
def read_int(data: bytes, offset: int, length: int, *, signed: bool = False) -> int:
|
|
43
44
|
return int.from_bytes(data[offset:offset + length], byteorder="big", signed=signed)
|
|
44
45
|
|
|
45
46
|
|
|
46
|
-
def convert_ptr(address: int, platform: str):
|
|
47
|
-
return address + Offsets.WIIU_BSS_OFFSET if platform == "wiiu" else address
|
|
48
|
-
|
|
49
|
-
|
|
50
47
|
def handle_gecko_errors(func: Callable[..., Any]) -> Any:
|
|
51
48
|
@wraps(func)
|
|
52
49
|
def wrapper(*args: Any, **kwargs: Any):
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
# Splatlogger (c) 2025 Shadow Doggo.
|
|
2
|
-
|
|
3
|
-
from enum import IntEnum
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class Pointers(IntEnum):
|
|
7
|
-
SESSION = 0x101E8980
|
|
8
|
-
STATIC_MEM = 0x101DD330
|
|
9
|
-
MAIN_MGR_BASE = 0x101E2814
|
|
10
|
-
MAIN_MGR_VS_GAME = 0x101E4FF8
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class Addresses(IntEnum):
|
|
14
|
-
# A bunch of data including player stats from the latest match somewhere on the stack.
|
|
15
|
-
# I'm not exactly sure what this is, but it works.
|
|
16
|
-
WIN_TEAM = 0x107AF914
|
|
17
|
-
STATS = 0x107AF944
|
|
18
|
-
CEMU_STACK_DEFAULT_WINDOWS = 0xE101440
|
|
19
|
-
CEMU_STACK_DEFAULT_LINUX = 0xE175900
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class Offsets(IntEnum):
|
|
23
|
-
WIIU_BSS_OFFSET = 0x503000
|
|
24
|
-
SESSION_LOBBY = 0xB0
|
|
25
|
-
SESSION_ID_IDX = 0xBD
|
|
26
|
-
SESSION_ID = 0xCC
|
|
27
|
-
LOBBY_PLAYER_LIST_HEAD = 0xA4
|
|
28
|
-
MAINMGR_PLAYER_MGR = 0x268
|
|
29
|
-
MAINMGR_SCENE_INFO = 0x2A4
|
|
30
|
-
PLAYERMGR_PLAYER_COUNT = 0x320
|
|
31
|
-
SCENE_INFO_RESULTS_SHOWN = 0x330
|
|
32
|
-
STATICMEM_PLAYER_INFO_ARY = 0x10
|
|
33
|
-
STATICMEM_STAGE = 0x28
|
|
34
|
-
STATICMEM_MATCH_HOUR = 0x234
|
|
35
|
-
STATICMEM_VERSUS_MODE = 0x238
|
|
36
|
-
STATICMEM_VERSUS_RULE = 0x23C
|
|
37
|
-
CEMU_WIN_TEAM = 0xF556C
|
|
38
|
-
CEMU_STATS = 0xF559C
|
|
39
|
-
PLAYER_INFO_NAME = 0x6
|
|
40
|
-
PLAYER_INFO_NAME_LENGTH = 0x20
|
|
41
|
-
PLAYER_INFO_REGION = 0x2C
|
|
42
|
-
PLAYER_INFO_TEAM = 0x33
|
|
43
|
-
PLAYER_INFO_GENDER = 0x37
|
|
44
|
-
PLAYER_INFO_SKIN_TONE = 0x3B
|
|
45
|
-
PLAYER_INFO_EYE_COLOR = 0x3F
|
|
46
|
-
PLAYER_INFO_WEAPON_SET = 0x42
|
|
47
|
-
PLAYER_INFO_MAIN_WEAPON = 0x46
|
|
48
|
-
PLAYER_INFO_SUB_WEAPON = 0x4A
|
|
49
|
-
PLAYER_INFO_SPECIAL_WEAPON = 0x4F
|
|
50
|
-
PLAYER_INFO_SHOES = 0x54
|
|
51
|
-
PLAYER_INFO_CLOTHES = 0x70
|
|
52
|
-
PLAYER_INFO_HEADGEAR = 0x8C
|
|
53
|
-
PLAYER_INFO_INK_TANK = 0xAA
|
|
54
|
-
PLAYER_INFO_LEVEL = 0xAF
|
|
55
|
-
PLAYER_INFO_RANK = 0xB3
|
|
56
|
-
PLAYER_INFO_FEST_TEAM = 0xB7
|
|
57
|
-
PLAYER_INFO_FEST_ID = 0xBA
|
|
58
|
-
PLAYER_INFO_FEST_RANK = 0xBF
|
|
59
|
-
PLAYER_INFO_PID = 0xD0
|
|
60
|
-
PLAYER_INFO_FEST_POWER = 0xD6
|
|
61
|
-
PLAYER_INFO_SIZE = 0xD8
|
|
62
|
-
STATS_BUFFER_LENGTH = 0x124
|
|
63
|
-
STATS_PLAYER_STRIDE = 0x20
|
|
64
|
-
STATS_POINTS = 0x3A
|
|
65
|
-
STATS_KILLS = 0x3E
|
|
66
|
-
STATS_DEATHS = 0x42
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
class VSMode(IntEnum):
|
|
70
|
-
REGULAR = 0
|
|
71
|
-
GACHI = 1
|
|
72
|
-
FEST = 2
|
|
73
|
-
PRIVATE = 3
|
|
74
|
-
PARTY = 4
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
class VSRule(IntEnum):
|
|
78
|
-
TURF = 0
|
|
79
|
-
RAINMAKER = 1
|
|
80
|
-
ZONES = 2
|
|
81
|
-
TOWER = 3
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|