Compare commits
79 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
74bfe9683d
|
|||
|
e88ed98318
|
|||
| 211d08710b | |||
| ee40425393 | |||
| 12f9a045ca | |||
| 8134243fb1 | |||
| c869ed27f1 | |||
| 321ba64016 | |||
| a8606d26a8 | |||
| a57cca6c12 | |||
|
86a46df442
|
|||
|
cdb397f5ea
|
|||
|
2b73d58033
|
|||
|
5abd47ff72
|
|||
|
261cb5c76f
|
|||
|
ea1044c271
|
|||
|
a1c358e13a
|
|||
|
1f00bf1d9b
|
|||
|
8bc602012e
|
|||
|
e0c34fe700
|
|||
|
cbe9747fd4
|
|||
|
de0104eec8
|
|||
|
c454068715
|
|||
| b8e7d81a22 | |||
|
d98e1f7478
|
|||
| d384cf4f11 | |||
| 681ce135f8 | |||
|
c497f64376
|
|||
|
5d43acd9a2
|
|||
|
90a1f0f938
|
|||
|
db52a15888
|
|||
|
71fc272133
|
|||
|
bef656077c
|
|||
|
fff0f3a71d
|
|||
|
e94fcf035e
|
|||
|
0abdb50eca
|
|||
|
a0306ecaee
|
|||
|
ce0aea1518
|
|||
|
1c3e8ca53c
|
|||
| e19c0b8bc9 | |||
| 6b636c4694 | |||
| 1fa3d18430 | |||
| 2f4a9462ea | |||
|
f05e68492d
|
|||
| 826eb88e2e | |||
| 80391df8b7 | |||
| 7f68d41bac | |||
| 75e7232096 | |||
| 7a29b39771 | |||
| cfcb56b13f | |||
| 9e6740b778 | |||
| 590f937959 | |||
| 31abc24e81 | |||
| 507fbf0451 | |||
| 1fa660129d | |||
| 67082f7b6b | |||
| b34c9a7c88 | |||
| 87e85932a4 | |||
| b704d827ed | |||
| 598ecf88e3 | |||
| a05971017d | |||
| b7819413b0 | |||
| 80e4b0e6fe | |||
| daee47b722 | |||
| 5724fb9a12 | |||
| 983753f840 | |||
| 53d2318873 | |||
| 83291d5501 | |||
| 55be92a56f | |||
| 6a23e5da0a | |||
| 4571cf7fdc | |||
| 4e3549ba6b | |||
| 3cbf64dda2 | |||
| 382bc6b7ce | |||
| e05991ffb3 | |||
| ffda97222d | |||
| 8f21604367 | |||
| 4045504b61 | |||
| 4aad1514c2 |
+12
-1
@@ -3,6 +3,7 @@
|
||||
|
||||
# Build products
|
||||
newserv
|
||||
newserv.exe
|
||||
src/Revision.cc
|
||||
|
||||
# CMake files
|
||||
@@ -37,12 +38,22 @@ system/teams/*.json
|
||||
# Files fuzziqersoftware uses that don't make sense to be committed to the main
|
||||
# repository
|
||||
*.dec
|
||||
*.WIP-s
|
||||
files
|
||||
make_release.py
|
||||
notes-private
|
||||
old-khyller
|
||||
old-newserv
|
||||
release
|
||||
release.zip
|
||||
all-quests
|
||||
system/dol
|
||||
system/patch-bb/data
|
||||
system/client-functions/Debug-Private
|
||||
system/config.2.json
|
||||
system/ep3/banners
|
||||
system/ep3/cardtex
|
||||
system/ep3/cardtex-trial
|
||||
system/players
|
||||
system/quests/includes
|
||||
system/quests/private
|
||||
.vscode
|
||||
|
||||
+6
-4
@@ -97,6 +97,7 @@ set(SOURCES
|
||||
src/LevelTable.cc
|
||||
src/Lobby.cc
|
||||
src/Loggers.cc
|
||||
src/MagEvolutionTable.cc
|
||||
src/Main.cc
|
||||
src/Map.cc
|
||||
src/Menu.cc
|
||||
@@ -134,11 +135,12 @@ set(SOURCES
|
||||
|
||||
add_executable(newserv ${SOURCES})
|
||||
target_include_directories(newserv PUBLIC ${ASIO_INCLUDE_DIR} ${Iconv_INCLUDE_DIRS})
|
||||
target_link_libraries(newserv phosg::phosg ${Iconv_LIBRARIES} pthread resource_file::resource_file)
|
||||
target_link_libraries(newserv phosg::phosg ${Iconv_LIBRARIES} resource_file::resource_file)
|
||||
if (WIN32)
|
||||
target_compile_definitions(newserv PUBLIC -DWINVER=0x0A00 -D_WIN32_WINNT=0x0A00)
|
||||
target_link_libraries(newserv ws2_32 mswsock bcrypt iphlpapi -static -static-libgcc -static-libstdc++)
|
||||
target_compile_options(newserv PRIVATE -Wa,-mbig-obj)
|
||||
target_compile_definitions(newserv PUBLIC WINVER=0x0A00 _WIN32_WINNT=0x0A00)
|
||||
target_compile_options(newserv PRIVATE -Wa,-mbig-obj -Wno-mismatched-new-delete)
|
||||
target_link_options(newserv PRIVATE -static -static-libgcc -static-libstdc++)
|
||||
target_link_libraries(newserv ws2_32 mswsock bcrypt iphlpapi)
|
||||
endif()
|
||||
add_dependencies(newserv newserv-Revision-cc)
|
||||
|
||||
|
||||
@@ -1,924 +1,33 @@
|
||||
# newserv <img align="right" src="static/s-newserv.png" />
|
||||
# PSO Peeps newserv
|
||||
|
||||
newserv is a game server, proxy, and reverse-engineering tool for Phantasy Star Online (PSO). **To quickly get started using newserv, just read the [server setup](#server-setup) and [how to connect](#how-to-connect) sections.**
|
||||
This is the PSO Peeps maintained version of [newserv](https://github.com/fuzziqersoftware/newserv), a game server, proxy, and reverse-engineering tool for Phantasy Star Online.
|
||||
|
||||
This project includes code that was reverse-engineered by the community in ages long past, and has been included in many projects since then. It also includes some game data from Phantasy Star Online itself, which was originally created by Sega.
|
||||
The original project was created by fuzziqersoftware and contains years of reverse-engineering, documentation, and implementation work for PSO. This repository keeps that work as the foundation while carrying local changes used by PSO Peeps.
|
||||
|
||||
Feel free to submit GitHub issues if you find bugs or have feature requests. I'd like to make the server as stable and complete as possible, but I can't promise that I'll respond to issues in a timely manner, because this is a personal project undertaken primarily for the fun of reverse-engineering. If you want to contribute to newserv yourself, pull requests are welcome as well.
|
||||
For the original upstream project, see:
|
||||
|
||||
See TODO.md for a list of known issues and future work I've curated, or go to the GitHub issue tracker for issues and requests submitted by the community.
|
||||
https://github.com/fuzziqersoftware/newserv
|
||||
|
||||
**Table of contents**
|
||||
* Background
|
||||
* [History](#history)
|
||||
* [Other server projects](#other-server-projects)
|
||||
* [Using newserv in other projects](#using-newserv-in-other-projects)
|
||||
* [Contributing to newserv](#contributing-to-newserv)
|
||||
* [Compatibility](#compatibility)
|
||||
* Setup
|
||||
* [Server setup](#server-setup)
|
||||
* [Client patch directories for PC and BB](#client-patch-directories)
|
||||
* [How to connect](#how-to-connect)
|
||||
* Features and configuration
|
||||
* [User accounts](#user-accounts)
|
||||
* [Installing quests](#installing-quests)
|
||||
* [Item tables and drop modes](#item-tables-and-drop-modes)
|
||||
* [Cross-version play](#cross-version-play)
|
||||
* [Server-side saves](#server-side-saves)
|
||||
* [Episode 3 features](#episode-3-features)
|
||||
* [Memory patches, client functions, and DOL files](#memory-patches-and-client-functions)
|
||||
* [Using newserv as a proxy](#using-newserv-as-a-proxy)
|
||||
* [Chat commands](#chat-commands)
|
||||
* [REST API](#rest-api)
|
||||
* [Non-server features](#non-server-features)
|
||||
## About this repository
|
||||
|
||||
# History
|
||||
This repository is used for PSO Peeps server development and deployment. Changes here may include server configuration support, compatibility fixes, event behavior, gameplay experiments, operational tooling, and other changes specific to PSO Peeps.
|
||||
|
||||
The history of this project essentially mirrors my development as a software engineer from the beginning of my hobby until now. If you don't care about the story, skip to the "Compatibility" or "Setup" sections below.
|
||||
Some changes may not be appropriate for upstream newserv or for general newserv deployments.
|
||||
|
||||
I originally purchased PSO GC when I heard about PSUL, and wanted to play around with running homebrew on my GameCube. This pathway eventually led to [GCARS-CS](https://github.com/fuzziqersoftware/gcars-cs), but that's another story.
|
||||
## Original README
|
||||
|
||||
<img align="left" src="static/s-khyps.png" /> After playing PSO for a while, both offline and online, I wrote a proxy called Khyps sometime in 2003. This was back in the days of the official Sega servers, where vulnerabilities weren't addressed in a timely manner or at all. It was common for malicious players using their own proxies or Action Replay codes (a story for another time) to send invalid commands that the servers would blindly forward, and cause the receiving clients to crash. These crashes were more than simply inconvenient; they could also corrupt your save data, destroying the hours of work you may have put into hunting items and leveling up your character.
|
||||
A copy of the upstream README is preserved here:
|
||||
|
||||
For a while it was essentially necessary to use a proxy to go online at all, so the proxy could block these invalid commands. Khyps was designed primarily with this function in mind, though it also implemented some convenient cheats, like the ability to give yourself or other players infinite HP and allow you to teleport to different places without using an in-game teleporter.
|
||||
[docs/upstream-README.md](docs/upstream-README.md)
|
||||
|
||||
<img align="left" src="static/s-khyller.png" /> After Khyps I took on the larger challenge of writing a server, which resulted in Khyller sometime in 2005. This was the first server of any type I had ever written. This project eventually evolved into a full-featured environment supporting all versions of the game that I had access to - at the time, PC, GC, and BB. (However, I suspect from reading the ancient source files that Khyller's BB support was very buggy.) As Khyller evolved, the code became increasingly cumbersome, littered with debugging filth that I never cleaned up and odd coding patterns I had picked up over the years. My understanding of the C++ language was woefully incomplete as well (as opposed to now, when it is still incomplete but not woefully so), which resulted in Khyller being essentially a C project that had a couple of classes in it.
|
||||
That document contains the original newserv history, setup notes, compatibility information, connection instructions, feature documentation, and technical reference material.
|
||||
|
||||
<img align="left" src="static/s-aeon.png" /> Sometime in 2006 or 2007, I abandoned Khyller and rebuilt the entire thing from scratch, resulting in Aeon. Aeon was substantially cleaner in code than Khyller but still fairly hard to work with, and it lacked a few of the more arcane features I had originally written (for example, the ability to convert any quest into a download quest). In addition, the code still had some stability problems... it turns out that Aeon's concurrency primitives were simply incorrect. I had derived the concept of a mutex myself, before taking any real computer engineering classes, but had implemented it incorrectly. I made the race window as small as possible, but Aeon would still randomly crash after running seemingly fine for a few days.
|
||||
## Building
|
||||
|
||||
At the time of its inception, Aeon was also called newserv, and you may find some beta releases floating around the Internet with filenames like `newserv-b3.zip`. I had released betas 1, 2, and 3 before I released the entire source of beta 5 and stopped working on the project when I went to college. This was around the time when I switched from writing software primarily on Windows to primarily on macOS and Linux, so Aeon beta 5 was the last server I wrote that specifically targeted Windows. (newserv, which you're looking at now, is difficult to compile on Windows but does work.)
|
||||
Build instructions are currently the same as upstream unless noted otherwise. See the original README for dependency and build details.
|
||||
|
||||
<img align="left" src="static/s-newserv.png" /> After a long hiatus from PSO and much professional and personal development in my technical abilities, I was reminiscing sometime in October 2018 by reading my old code archives. Somehow inspired when I came across Aeon, I spent a weekend and a couple more evenings rewriting the entire project again, cleaning up ancient patterns I had used eleven years ago, replacing entire modules with simple STL containers, and eliminating even more support files in favor of configuration autodetection. The code is now suitably modern and stable, and I'm not embarrassed by its existence, as I am by Aeon beta 5's source code and my archive of Khyller (which, thankfully, no one else ever saw).
|
||||
## License and attribution
|
||||
|
||||
## Other server projects
|
||||
This project remains based on newserv by fuzziqersoftware.
|
||||
|
||||
Independently of this project, there are many other PSO servers out there. Those that I know of that are (or were) public are listed here in approximate chronological order:
|
||||
|
||||
* (Early 2000s) **[Schtserv](https://schtserv.com/)**: The first public-access PSO server, written in Delphi by Schthack. Schtserv is the only other unofficial server to support Episode 3, their implementation of which is based on newserv's (which is based on Sega's).
|
||||
* (2005) **Khyller**: An early attempt of mine to support PSO PC, GC, and BB. See above for more details.
|
||||
* (2006) **Aeon**: My second attempt. Better than Khyller, but still unreliable.
|
||||
* (2008) **Tethealla**: A fairly extensive implementation of PSOBB, written in C by Sodaboy. The public version of Tethealla has been [officially disowned](https://www.pioneer2.net/community/threads/tethealla-server-forums-removal.26365/) as it is now more than 15 years old, but closed-source development continues. [Ephinea](https://ephinea.pioneer2.net/) is the continuation of this project. Several other modern PSOBB servers are forks of the initial public version of Tethealla as well.
|
||||
* (2008) **[Sylverant](https://sylverant.net/)** [(source)](https://sourceforge.net/projects/sylverant/): The second public-access PSO server, written in C by BlueCrab.
|
||||
* (2015) **[Archon](https://github.com/dcrodman/archon)**: A PSOBB server written in Go by Drew Rodman.
|
||||
* (2015) **[Idola](https://github.com/HybridEidolon/idolapsoserv)**: A PSOBB server written in Rust by HybridEidolon. Functionality status unknown; the project has been archived.
|
||||
* (2017) **[Aselia](https://github.com/Solybum/Aselia)**: A PSOBB server written in C# by Soly. It seems this was planned to be open-source at some point, but that has not (yet) happened.
|
||||
* (2018) **newserv**: This project right here.
|
||||
* (2019) **[Mechonis](https://gitlab.com/sora3087/mechonis)**: A PSOBB server with a microservice architecture written in TypeScript by TrueVision.
|
||||
* (2020) **[Booma.Server](https://github.com/HelloKitty/Booma.Server)**: A PSOBB server written in C# by Glader, with Soly's help.
|
||||
* (2021) **[Phantasmal World](https://github.com/DaanVandenBosch/phantasmal-world)**: A set of PSO tools, including a web-based model viewer and quest builder, and a PSO server, written by Daan Vanden Bosch.
|
||||
* (2021) **[Elseware](http://git.sharnoth.com/jake/elseware)**: A PSOBB server written in Rust by Jake.
|
||||
|
||||
## Using newserv in other projects
|
||||
|
||||
You are free to use code from newserv in your own open-source projects; the only condition is that the contents of the LICENSE file must be included in your project if you use code from newserv. Your project does not also have to use the MIT license; you can use any license you want.
|
||||
|
||||
If you want to use parts of newserv in your project, there are two easy ways to do so with proper licensing:
|
||||
* If you're using a lot of code from newserv, you can put a copy of newserv's LICENSE file in your repository alongside your own license file, or include the contents of newserv's license in your own license file.
|
||||
* If you're only using a few files from newserv, you can copy and paste the contents of the LICENSE file into a comment at the beginning of each copied file.
|
||||
|
||||
Some of the more likely useful files are:
|
||||
* **src/CommandFormats.hh**: Complete listing of all network commands used in all known versions of the game, and their formats
|
||||
* **src/CommonItemSet.hh/cc**: Format of ItemPT files, shop definition files, and tekker adjustment tables
|
||||
* **src/DCSerialNumbers.hh/cc**: PSO DC serial number validation algorithm and serial number generator
|
||||
* **src/ItemData.hh**: Item format reference
|
||||
* **src/ItemCreator.hh/cc**: Reverse-engineered item generator from Episodes 1&2 (used for all versions)
|
||||
* **src/ItemParameterTable.hh**: Format of many structures in ItemPMT.prs
|
||||
* **src/Map.hh/cc**: Map file (.dat/.evt) structure, listing of object/enemy types and parameters, and reverse-engineered Challenge Mode random enemy generation algorithm
|
||||
* **src/QuestScript.cc**: Complete listing of all quest opcodes on all versions, along with their arguments and behavior
|
||||
* **src/RareItemSet.hh/cc**: Format of ItemRT files (rare item drop tables)
|
||||
* **src/SaveFileFormats.hh**: Definitions of save file structures for all versions
|
||||
* **src/Episode3/DataIndexes.hh**: Episode 3 file structures, including card definition format and map/quest format
|
||||
* **system/item-tables/names-v4.json**: Names of all items, indexed by the first 3 bytes of data1
|
||||
|
||||
## Contributing to newserv
|
||||
|
||||
The goals of this project are:
|
||||
* Build stable, extensible PSO server software that includes all vanilla functionality as well as optional modern conveniences, features, and cheats.
|
||||
* Document the internals of PSO's network protocol, file formats, and game mechanics. This is mainly done through comments in the code.
|
||||
|
||||
This is a personal project; there is no official development team, official website, or official instance of newserv. Issues and pull requests are certainly welcome, but please only add content (e.g. quests or patches) that you've created, is already public, or you have permission to release publicly.
|
||||
|
||||
# Compatibility
|
||||
|
||||
newserv supports all known versions of PSO, including various development prototypes. This table lists all versions that newserv supports. (NTE stands for Network Trial Edition; the GameCube beta versions were called Trial Edition instead, but we use the NTE abbreviation anyway for consistency.)
|
||||
|
||||
| Version | Lobbies | Games | Proxy |
|
||||
|-----------------|----------|----------|----------|
|
||||
| DC NTE | Yes | Yes | Yes |
|
||||
| DC 11/2000 | Yes | Yes | Yes |
|
||||
| DC 12/2000 | Yes | Yes | Yes |
|
||||
| DC 01/2001 | Yes | Yes | Yes |
|
||||
| DC V1 | Yes | Yes | Yes |
|
||||
| DC 08/2001 | Yes | Yes | Yes |
|
||||
| DC V2 | Yes | Yes | Yes |
|
||||
| PC NTE | Yes (1) | Yes | Yes |
|
||||
| PC | Yes | Yes | Yes |
|
||||
| GC Ep1&2 NTE | Yes | Yes | Yes |
|
||||
| GC Ep1&2 | Yes | Yes | Yes |
|
||||
| GC Ep1&2 Plus | Yes | Yes | Yes |
|
||||
| GC Ep3 NTE | Yes | Yes (2) | Yes |
|
||||
| GC Ep3 | Yes | Yes | Yes |
|
||||
| Xbox Ep1&2 Beta | Yes (3) | Yes (3) | Yes (3) |
|
||||
| Xbox Ep1&2 | Yes (3) | Yes (3) | Yes (3) |
|
||||
| BB (vanilla) | Yes | Yes | Yes |
|
||||
| BB (Tethealla) | Yes | Yes | Yes |
|
||||
|
||||
*Notes:*
|
||||
1. *This is the only version of PSO that doesn't have any way to identify the player's account - there is no serial number or username. For this reason, AllowUnregisteredUsers must be enabled in config.json to support PC NTE, and PC NTE players receive a random Guild Card number every time they connect. To prevent abuse, PC NTE support can be disabled in config.json.*
|
||||
2. *Episode 3 NTE battles are not well-tested; some things may not work. See notes/ep3-nte-differences.txt for a list of known differences between NTE and the final version. NTE and non-NTE players cannot battle each other.*
|
||||
3. *PSO Xbox connects through Xbox Live, so you can't easily host a private server for this version of the game. See the [How to connect](#pso-xbox) section.*
|
||||
|
||||
# Setup
|
||||
|
||||
## Server setup
|
||||
|
||||
Currently newserv works on macOS, Windows, and Ubuntu Linux. It will likely work on other Linux flavors too.
|
||||
|
||||
### Windows/macOS
|
||||
|
||||
1. Download the latest release.zip file from the [releases page](https://github.com/fuzziqersoftware/newserv/releases).
|
||||
2. Extract the contents of the archive to some location on your computer.
|
||||
3. Go into the system/ folder, open config.json in a text editor, and edit it to your liking. There are comments in the file that describe what all the options do. Most of the options can be left alone if you want default behavior, but on Windows, you must change LocalAddress and ExternalAddress.
|
||||
4. (Optional) If you plan to play Blue Burst on newserv, set up the patch directory. See [client patch directories](#client-patch-directories) for details.
|
||||
5. Run the newserv executable.
|
||||
|
||||
### Linux
|
||||
|
||||
There are currently no precompiled releases for Linux. To run newserv on Linux, you'll have to build it from source - see the section below.
|
||||
|
||||
### Building from source (macOS/Linux)
|
||||
|
||||
To build on macOS or Linux:
|
||||
|
||||
1. Install the dependencies needed for your platform:
|
||||
* macOS: `brew install cmake asio libiconv`
|
||||
* Linux: `sudo apt-get install cmake libasio-dev` (or use your Linux distribution's package manager)
|
||||
2. Build and install [phosg](https://github.com/fuzziqersoftware/phosg) and [resource_dasm](https://github.com/fuzziqersoftware/resource_dasm).
|
||||
3. Run `cmake . && make` in the newserv directory.
|
||||
|
||||
After building newserv, edit system/config.example.json as needed **and rename it to system/config.json** (note that this step is not necessary for the precompiled releases), set up [client patch directories](#client-patch-directories) if you're planning to play Blue Burst, then run `./newserv` in newserv's directory.
|
||||
|
||||
The server has an interactive shell which can be used to make changes, such as managing user accounts, updating the server's configuration, managing Episode 3 tournaments, and more. Type `help` and press Enter to see all the commands.
|
||||
|
||||
On Linux and macOS, the server also responds to SIGUSR1 and SIGUSR2. SIGUSR1 does the equivalent of the shell's `reload config` command, which reloads config.json but not any dependent files (so quests, Episode 3 maps, etc. will not be reloaded). SIGUSR2 does the equivalent of the shell's `reload all` command, which reloads everything.
|
||||
|
||||
To use newserv in other ways (e.g. for translating data), see the end of this document.
|
||||
|
||||
### Building from source (Windows)
|
||||
|
||||
The current version of newserv is cross-compiled using mingw-w64 on a macOS build machine, with the necessary libraries manually installed. Setting up such a build environment is tedious and not recommended; it's recommended to just use a release version instead.
|
||||
|
||||
Here is a rough outline of the Windows build process. You should only attempt this yourself if you're familiar with setting up build environments and can deal with issues you may encounter along the way.
|
||||
1. Install recent versions of MinGW and CMake.
|
||||
2. Build and install zlib, libiconv, asio, phosg, and resource_dasm into your MinGW environment.
|
||||
3. Clone the newserv repository with symlinks enabled: `git clone -c core.symlinks=true https://github.com/fuzziqersoftware/newserv.git`
|
||||
4. Build newserv via CMake.
|
||||
|
||||
## Client patch directories
|
||||
|
||||
newserv implements a patch server for PSO PC and PSO BB game data. Any file or directory you put in the system/patch-bb or system/patch-pc directories will be synced to clients when they connect to the patch server.
|
||||
|
||||
For Blue Burst set up, the below is mandatory for a smooth experience:
|
||||
|
||||
1. Browse to your chosen client's data directory.
|
||||
2. Copy all the `map_*.dat` files, `map_*.evt`, `unitxt_*` files, and the `data.gsl` file and place them in `system/patch-bb/data`.
|
||||
3. If you're using game files from the Tethealla client, make a copy of `unitxt_j.prs` inside system/patch-bb/data and name it `unitxt_e.prs`. (If `unitxt_e.prs` already exists, replace it with the copied file.)
|
||||
|
||||
If you don't have a BB client, or if you're using a Tethealla client from another source, Tethealla clients that are compatible with newserv can be found here: [English](https://web.archive.org/web/20240402011115/https://ragol.org/files/bb/TethVer12513_English.zip) / [Japanese](https://web.archive.org/web/20240402013127/https://ragol.org/files/bb/TethVer12513_Japanese.zip). These clients connect to 127.0.0.1 (localhost) automatically.
|
||||
|
||||
For BB clients, newserv reads some files out of the patch data to implement game logic, so it's important that certain game files are synchronized between the server and the client. newserv contains defaults for these files in the system/maps/bb-v4 directory, but if these don't match the client's copies of the files, odd behavior will occur in games.
|
||||
|
||||
To make server startup faster, newserv caches the modification times, sizes, and checksums of the files in the patch directories. If the patch server appears to be misbehaving, try deleting the .metadata-cache.json file in the relevant patch directory to force newserv to recompute all the checksums. Also, in the case when checksums are cached, newserv may not actually load the data for a patch file until it's needed by a client. Therefore, modifying any part of the patch tree while newserv is running can cause clients to see an inconsistent view of it.
|
||||
|
||||
Patch directory contents are cached in memory. If you've changed any of these files, you can run `reload patch-indexes` in the interactive shell to make the changes take effect without restarting the server.
|
||||
|
||||
## How to connect
|
||||
|
||||
### PSO DC
|
||||
|
||||
Depending on the version of PSO DC that you have, the instructions to connect to a newserv instance will vary.
|
||||
|
||||
If you have NTE, USv1, EUv1, or EUv2 and a Broadband Adapter, edit the broadband DNS address to newserv's IP address with newserv's DNS server running. Otherwise, it is necessary to patch the disc or use a codebreaker code to remove the Hunter License server check and/or redirect PSO to the newserv instance. Patching the disc or creating a codebreaker code is beyond the scope of this document.
|
||||
|
||||
### PSO DC on Flycast
|
||||
|
||||
If you're emulating PSO DC, the NTE, USv1, EUv1, and EUv2 versions will connect to newserv by setting the following options in Flycast's `emu.cfg` file under `[network]`:
|
||||
- DNS = Your newserv's server address (newserv's DNS server must be running on port 53)
|
||||
- EmulateBBA = yes
|
||||
- Enable = yes
|
||||
|
||||
It is also necessary to save any DNS information to the flash memory of the Dreamcast to use the BBA - the easiest way to do this is to use the website option in USv2 and then choose the save to flash option.
|
||||
|
||||
If the server is running on the same machine as Flycast, this might not work, even if you point Flycast's DNS queries at your local IP address (instead of 127.0.0.1). In this case, you can modify the loaded executable in memory to make it connect anywhere you want. There is a script included with newserv that can do this on macOS; a similar technique could be done manually using scanmem on Linux or Cheat Engine on Windows. To use the script, do this:
|
||||
1. Build and install [memwatch](https://github.com/fuzziqersoftware/memwatch).
|
||||
2. Start Flycast and run PSO. (You must start PSO before running the script; it won't work if you run the script before loading the game.)
|
||||
3. Run `sudo patch_flycast_memory.py <original-destination>`. Replace `<original-destination>` with the hostname that PSO wants to connect to (you can find this out by using Wireshark and looking for DNS queries). The script may take up to a minute; you can continue using Flycast while it runs, but don't start an online game until the script is done.
|
||||
4. Run newserv and start an online game in PSO.
|
||||
|
||||
If you use this method, you'll have to run the script every time you start PSO in Flycast, but you won't have to run it again if you start another online game without restarting emulation.
|
||||
|
||||
If using JPv1, JPv2, or USv2, it is also necessary to remove the Hunter Licence server check, either with a disc patch or codebreaker code. Patching the disc or creating a codebreaker code is beyond the scope of this document.
|
||||
|
||||
### PSO PC
|
||||
|
||||
PSO PC has its connection addresses in `pso.exe`. Hex edit the executable with the connection address you want to connect to. Common server addresses to search for to replace are:
|
||||
- pso20.sonic.isao.net
|
||||
- sg207634.sonicteam.com
|
||||
- pso-mp01.sonic.isao.net
|
||||
- gsproduc.ath.cx
|
||||
- sylverant.net
|
||||
|
||||
The version of PSO PC I have has the server addresses starting at offset 0x29CB34 in pso.exe. Change those addresses to "localhost" (without quotes) if you just want to connect to a locally-running newserv instance. Alternatively, you can add an entry to the Windows hosts file (C:\Windows\System32\drivers\etc\hosts) to redirect the connection to 127.0.0.1 (localhost) or any other IP address.
|
||||
|
||||
### PSO GC on a real GameCube
|
||||
|
||||
You can make PSO connect to newserv by setting the default gateway and DNS server addresses in the game's network settings to newserv's address. newserv's DNS server must be running on port 53 and must be accessible to the GameCube. If you're not playing PSO Plus or Episode III, this should be all you need to do, assuming you already set LocalAddress in config.json to your PC's private IP address.
|
||||
|
||||
If you have PSO Plus or Episode III, it won't want to connect to a server on the same local network as the GameCube itself, as determined by the GameCube's IP address and subnet mask. There are a couple of ways to get around this.
|
||||
|
||||
Sodaboy described a fairly easy method, which is to forward the PSO and DNS ports in your router's configuration to your PC's private IP address (the PSO ports are in config.json, and are all TCP; the DNS port is 53 and is UDP). Then, set LocalAddress and ExternalAddress in config.json to your external IP address (from e.g. whatismyip.com). Most routers will let you connect to your public IP address even from within the local network, but the GameCube will think it's connecting to a different network, so it won't reject the connection. If you're concerned about security and don't want your server to be publicly accessible, you can use Windows Firewall or UFW on Linux block incoming connections on the ports you opened, except for connections from the IP addresses you specify.
|
||||
|
||||
Another method is to use two network interfaces on the same PC, and tell the GameCube to connect to the one that appears to be on a different network. For example, if your GameCube is on the 10.0.0.x subnet and your PC's address is 10.0.0.5, you can create a fake network adapter on your PC (or use an existing real one) that has an IP address on a different subnet than the GameCube, such as 192.168.0.8. Then, in PSO's network config, set the default gateway and DNS server addresses to 192.168.0.8, and set LocalAddress in config.json to 192.168.0.8, and PSO should connect. This is what I did back in the old days when I primarily developed software on Windows, but I haven't tried it in many years.
|
||||
|
||||
### PSO GC on a Wii or Wii U
|
||||
|
||||
Using a Wii or Wii U to connect to newserv requires the Wii or vWii to be softmodded. How to do this is beyond the scope of this document.
|
||||
|
||||
Nintendont includes BBA emulation and is compatible with all PSO GameCube versions except Episodes I&II Trial Edition. To use Nintendont, enable BBA emulation in Nintendont's settings and follow the instructions in the above section (PSO GC on a real GameCube).
|
||||
|
||||
Devolution includes modem emulation and is compatible with all PSO GameCube versions including Episodes I&II Trial Edition. newserv can act as a PPP server, which Devolution can directly connect to. To do this:
|
||||
1. Enable the PPPRawListen option according to the comments in config.json.
|
||||
2. Start newserv.
|
||||
3. In the game's network settings, set the username and password to anything (they cannot be blank), and set the phone number to the number that newserv outputs to the console during startup. (It will be near the end of all the startup log messages.) If your Wii is on the same network as newserv, use the local number; otherwise, use the external number.
|
||||
|
||||
### PSO GC on Dolphin
|
||||
|
||||
If you're using the HLE BBA type, set the BBA's DNS server address to newserv's IP address and it should work. (If newserv is on the same machine as Dolphin, you will need to use an Action Replay code directed at 127.0.0.1 to connect, as PSO rejects DNS queries from the same IP address.) Set PSO's network settings the same as listed below.
|
||||
|
||||
If you're using the TAP (not tapserver) BBA type, you'll have to set PSO's network settings appropriately for your tap interface. Set the DNS server address in PSO's network settings to newserv's IP address.
|
||||
|
||||
If you're using the tapserver BBA or modem type, you can make it connect to a newserv instance running on the same machine via the tapserver interface. To do this:
|
||||
1. In the GameCube pane of the Config window, set the SP1 device to Broadband Adapter (tapserver) or Modem Adapter (tapserver).
|
||||
2. Click the "..." button next to the SP1 menu. If you're using the tapserver BBA, enter `127.0.0.1:5059` in the box. If you're using the tapserver modem, enter `127.0.0.1:5058` in the box. (If newserv isn't running on the same machine as Dolphin, replace 127.0.0.1 with newserv's IP address.)
|
||||
3. In PSO's network settings, enable DHCP ("Automatically obtain an IP address"), set DNS server address to "Automatic", and leave DHCP Hostname as "Not set". Leave the proxy server settings blank.
|
||||
4. Start an online game.
|
||||
|
||||
### PSO Xbox
|
||||
|
||||
Unfortunately, you can't easily host a private server for PSO Xbox because the Xbox version of the game tunnels its connections through Xbox Live. There is a modern replacement for Xbox Live named [Insignia](https://insignia.live/), which supports the three main PSO Xbox servers, but as of now does not support other private PSO servers.
|
||||
|
||||
### PSO BB
|
||||
|
||||
The PSO BB client has been modified and distributed in many different forms. newserv supports most, but not all, of the common distributions. Unlike other versions, it's common for various BB clients to have different map files. It's important that the client and server have the same map files, so make sure to set up the patch directory based on the client you'll be using with newserv. (See the [client patch directories](#client-patch-directories) section for instructions on setting this up.)
|
||||
|
||||
The original Japanese and US versions of PSO BB work with newserv (the last Japanese release can be found [here](https://archive.org/details/psobb_jp_setup_12511_20240109/)). To get them to connect to your server, do one of the following:
|
||||
* Use a drop-in patcher like [AzureFlare](https://github.com/Repflez/AzureFlare).
|
||||
* Edit your hosts file to redirect the client's destination address to localhost or your server's address.
|
||||
* Edit psobb.exe to point to your newserv instance. The original clients are packed with various versions of ASProtect, so this is a more involved process than simply opening the executable in a hex editor and finding/replacing some strings.
|
||||
|
||||
Alternatively, you can use the Tethealla client ([English](https://web.archive.org/web/20240402011115/https://ragol.org/files/bb/TethVer12513_English.zip) or [Japanese](https://web.archive.org/web/20240402013127/https://ragol.org/files/bb/TethVer12513_Japanese.zip)). If the server is on the same PC as the client and you don't plan to have any external players, these Tethealla clients will automatically connect to the server without any modifications. This version of the client is not packed, and you can find the connection addresses starting at offset 0x56D724 in psobb.exe. Overwrite these addresses with your server's hostname or IP address, and you should be able to connect.
|
||||
|
||||
### Allowing external players to connect
|
||||
|
||||
If you want to accept connections from outside your local network, you'll need to set ExternalAddress to your public IP address in the configuration file, and you'll likely need to open some ports in your router's NAT configuration - specifically, all the TCP ports listed in PortConfiguration in config.json.
|
||||
|
||||
For GC clients, you'll have to use newserv's built-in DNS server or set up your own DNS server as well. If you want external clients to be able to use your DNS server, you'll have to forward UDP port 53 to your newserv instance. Remote players can then connect to your server by entering your DNS server's IP address in their client's network configuration.
|
||||
|
||||
# Server feature configuration
|
||||
|
||||
## User accounts
|
||||
|
||||
By default, newserv does not require users to pre-register before playing; the server will instead automatically create an account the first time each player connects. These accounts have no special permissions. You can view, create, edit, and delete user accounts in the server's shell (run `help` in the shell to see how to do this).
|
||||
|
||||
A license is a set of credentials that a player can use to log in. There are six types of licenses:
|
||||
* *DC NTE licenses* consist of a 16-character serial number and 16-character access key.
|
||||
* *DC licenses* consist of an 8-character hex serial number and an 8-character access key.
|
||||
* *PC licenses* are the same format as DC licenses, but are used for PC v2.
|
||||
* *GC licenses* consist of a 10-digit decimal serial number, a 12-character access key, and a password of up to 8 characters.
|
||||
* *XB licenses* consist of a gamertag of up to 16 characters, a 16-character hex user ID, and a 16-character hex account ID.
|
||||
* *BB licenses* consist of a username of up to 16 characters and a password of up to 16 characters.
|
||||
|
||||
Each account may have multiple licenses. To add a license to an existing account, use `add-license` in the shell.
|
||||
|
||||
On BB, character data is scoped to the license, but system and Guild Card data is scoped to the account. That is, an account with multiple BB licenses can have more than 4 characters (up to 4 per license), but they will all share the same team membership and Guild Card lists.
|
||||
|
||||
You may want to give your account elevated privileges. To do so, run `update-account ACCOUNT-ID flags=root` (replacing ACCOUNT-ID with your actual account-id). You can also use update-account to edit other parts of the account; see the help text for more information.
|
||||
|
||||
## Installing quests
|
||||
|
||||
newserv automatically finds quests in the subdirectories of the system/quests/ directory. To install your own quests, or to use quests you've saved using the proxy's Save Files option, just put them in one of the subdirectories there and name them appropriately. The subdirectories and their behaviors (e.g. in which game modes they should appear and for which PSO versions) is defined in the QuestCategories field in config.json.
|
||||
|
||||
Within the category directories, quest files should be named like `q###-VERSION-LANGUAGE.EXT` (although the `q` is ignored, and can be any letter). The fields in each filename are:
|
||||
- `###`: quest number (this doesn't really matter; it should just be unique across the PSO version)
|
||||
- `VERSION`: dn = Dreamcast NTE, dp = Dreamcast 11/2000 prototype, d1 = Dreamcast v1, dc = Dreamcast v2, pcn = PC NTE, pc = PC, gcn = GameCube NTE, gc = GameCube Episodes 1 & 2, gc3 = Episode 3 (see below), xb = Xbox, bb = Blue Burst
|
||||
- `LANGUAGE`: j = Japanese, e = English, g = German, f = French, s = Spanish
|
||||
- `EXT`: file extension (see table below)
|
||||
|
||||
For .dat files, the `LANGUAGE` token may be omitted. If it's present, then that .dat file will only be used for that language of the quest; if omitted, then that .dat file will be used for all languages of the quest.
|
||||
|
||||
For example, the GameCube version of Lost HEAT SWORD is in two files named `q058-gc-e.bin` and `q058-gc.dat`. newserv knows these files are quests because they're in the system/quests/ directory, it knows they're for PSO GC because the filenames contain `-gc`, it knows this is the English version of the quest because the .bin filename ends with `-e` (even though the .dat filename does not), and it puts them in the Retrieval category because the files are within the retrieval/ directory within system/quests/.
|
||||
|
||||
Some quests have additional JSON metadata files that describe how the server should handle them. These metadata files are generally named similarly to their .bin and .dat counterparts, except the `VERSION` token may also be omitted if the metadata applies to all languages of the quest on all PSO versions. See the comments in system/quests/retrieval/q058.json for all of the available options and how to use them. Some of the options are:
|
||||
- Disable or hide the quest if certain preceding quests aren't cleared or other conditions aren't met
|
||||
- Enable the quest to be joined while in progress
|
||||
- Override the common and/or rare item tables and set the allowed drop modes
|
||||
|
||||
Some quests may also include a .pvr file, which contains an image used in the quest. These files are named similarly to their .bin and .dat counterparts.
|
||||
|
||||
The GameCube and Xbox quest formats are very similar, but newserv treats them as different. If you want to use the same quest file for GameCube and Xbox clients, you can make one a symbolic link to the other.
|
||||
|
||||
There are multiple PSO quest formats out there; newserv supports all of them. It can also decode any known format to standard .bin/.dat format. Specifically:
|
||||
|
||||
| Format | Extension | Supported | Decode action |
|
||||
|------------------|-----------------------|------------|------------------|
|
||||
| Compressed | .bin and .dat | Yes | None (1) |
|
||||
| Compressed Ep3 | .bin or .mnm | Yes (4) | None (1) |
|
||||
| Uncompressed | .bind and .datd | Yes | compress-prs (2) |
|
||||
| Uncompressed Ep3 | .bind or .mnmd | Yes (4) | compress-prs (2) |
|
||||
| Source | .bin.txt and .dat | Yes | None (5) |
|
||||
| VMS (DCv1) | .bin.vms and .dat.vms | Yes | decode-vms |
|
||||
| VMS (DCv2) | .bin.vms and .dat.vms | Decode (3) | decode-vms (3) |
|
||||
| GCI (decrypted) | .bin.gci and .dat.gci | Yes | decode-gci |
|
||||
| GCI (with key) | .bin.gci and .dat.gci | Yes | decode-gci |
|
||||
| GCI (no key) | .bin.gci and .dat.gci | Decode (3) | decode-gci (3) |
|
||||
| GCI (Ep3 NTE) | .bin.gci or .mnm.gci | Decode (3) | decode-gci (3) |
|
||||
| GCI (Ep3) | .bin.gci or .mnm.gci | Yes | decode-gci |
|
||||
| DLQ | .bin.dlq and .dat.dlq | Yes | decode-dlq |
|
||||
| DLQ (Ep3) | .bin.dlq or .mnm.dlq | Yes | decode-dlq |
|
||||
| QST (online) | .qst | Yes | decode-qst |
|
||||
| QST (download) | .qst | Yes | decode-qst |
|
||||
|
||||
*Notes:*
|
||||
1. *This is the default format. You can convert these to uncompressed format by running `newserv decompress-prs FILENAME.bin FILENAME.bind` (and similarly for .dat -> .datd)*
|
||||
2. *Similar to (1), to compress an uncompressed quest file: `newserv compress-prs FILENAME.bind FILENAME.bin` (and likewise for .datd -> .dat)*
|
||||
3. *Use the decode action to convert these quests to .bin/.dat format before putting them into the server's quests directory. If you know the encryption seed (serial number), pass it in as a hex string with the `--seed=` option. If you don't know the encryption seed, newserv will find it for you, which will likely take a long time.*
|
||||
4. *Episode 3 quests don't go in the system/quests directory. See the [Episode 3 section](#episode-3-features) section below.*
|
||||
5. *Quest source can be assembled into a .bin or .bind file with `newserv assemble-quest-script FILENAME.txt`. See system/quests/retrieval/q058-gc-e.bin.txt for an annotated example; this is the English GameCube version of Lost HEAT SWORD.*
|
||||
|
||||
Episode 3 download quests consist only of a .bin file - there is no corresponding .dat file. Episode 3 download quest files may be named with the .mnm extension instead of .bin, since the format is the same as the standard map files (in system/ep3/maps/). These files can be encoded in any of the formats described above, except .qst.
|
||||
|
||||
When newserv indexes the quests during startup, it will warn (but not fail) if any quests are corrupt or in unrecognized formats.
|
||||
|
||||
Quest contents are cached in memory, but if you've changed the contents of the quests directory, you can re-index the quests without restarting the server by running `reload quest-index` in the interactive shell. The new quests will be available immediately, but any games with quests already in progress will continue using the old versions of the quests until those quests end.
|
||||
|
||||
## Item tables and drop modes
|
||||
|
||||
newserv supports server-side item generation on all game versions, except for the earliest DC prototypes (NTE and 11/2000). By default, the game behaves as it did on the original servers - on all versions except BB, item drops are controlled by the leader client in each game, and on BB, item drops are controlled by the server.
|
||||
|
||||
There are five different available behaviors for item drops:
|
||||
* `disabled` (or `none`): No items will drop from boxes or enemies.
|
||||
* `client`: The game leader generates items, all items are visible to all players, and any player may pick up any item. This is the default mode for all game versions, except this mode cannot be used if the game leader is on BB.
|
||||
* `shared`: The server generates items, all items are visible to all players, and any player may pick up any item. This is the default mode if the game leader is on BB.
|
||||
* `private`: The server generates items, but each player may get a different item from any box or enemy. If a player isn't in the same area as an enemy at the time it's defeated, they won't get any item from it. Items dropped by players are visible to everyone.
|
||||
* `duplicate`: The server generates items, and each player will get the same item from any box or enemy, but there is one copy of each item for each player (and each player only sees their own copy of the item). If a player isn't in the same area as an enemy at the time it's defeated, they won't get any item from it. Items dropped by players are not duplicated and are visible to everyone.
|
||||
|
||||
In the `private` and `duplicate` modes, there is no incentive to pick up items before another player, since other players cannot pick up the items you see dropped from boxes and enemies. However, if you pick up an item and drop it later, it can then be seen and picked up by any player.
|
||||
|
||||
The drop mode can be changed at any time during a game with the `$dropmode` chat command. If the mode is changed after some items have already been dropped, the existing items retain their visibility (that is, items dropped in private mode still can't be picked up by other players since they were dropped before the mode was changed). You can configure which drop modes are used by default, and which modes players are allowed to choose, in config.json. See the comments above the AllowedDropModes and DefaultDropMode keys.
|
||||
|
||||
In the server drop modes, the item tables used to generate common items are in the `system/item-tables/ItemPT-*` files. (The V2 files are used for V1 as well.) The rare item tables are in the `rare-table-*.json` files. Unlike the original formats, it's possible to make each enemy drop multiple different rare items at different rates, though the default tables never do this.
|
||||
|
||||
## Cross-version play
|
||||
|
||||
All versions of PSO can see and interact with each other in the lobby. By default, newserv allows V1 and V2 players to play in games together, and allows GC and Xbox players to play in games together. You can change these rules to allow all versions to play in games together, or to prevent versions from playing in games together, with the CompatibilityGroups setting in config.json.
|
||||
|
||||
There are several cross-version restrictions that always apply regardless of the compatibility groups setting:
|
||||
* DC V1 players cannot join DC V2 games if the game creator didn't choose to allow them.
|
||||
* DC V1 players cannot join games if the difficulty level is set to Ultimate or the game mode is Battle or Challenge.
|
||||
* Only GC, Xbox, and BB players can join games in Episode 2.
|
||||
* Only BB players can join games in Episode 4.
|
||||
* Episode 3 players cannot join non-Episode 3 games, and vice versa.
|
||||
|
||||
V1/V2 compatibility and GC/Xbox compatibility are well-tested, but other situations are not. Not much attention has been given yet to how items should be handled across major versions; if you enable V2/GC compatibility, for example, there will likely be bugs. Please report such bugs as GitHub issues.
|
||||
|
||||
In cross-version play, when any of the server drop modes are used, the server uses the drop tables corresponding to the leader's version and section ID. (For example, if a DC V1 player is the game leader, rare-table-v1.json will be used, even after V2 players join.) If a BB player is the leader and the `client` drop mode is used, the server generates items as if it were in `shared` mode.
|
||||
|
||||
## Server-side saves
|
||||
|
||||
newserv has the ability to save character data on the server side. For PSO BB, this is required of course, but this feature can also be used on other PSO versions.
|
||||
|
||||
Each account has 4 BB character slots and 16 non-BB character file slots. The non-BB slots are independent of the BB slots, and can be accessed with the `$savechar <slot>` and `$loadchar <slot>` commands (slots are numbered 1 through 16). `$savechar` copies the character you're currently playing as and saves the data on the server, and `$loadchar` does the reverse, overwriting your current character with the data saved on the server. Note that you can load a character that was saved from a different version of PSO, which allows you to easily transfer characters between games. On v1 and v2, changes done by `$loadchar` will be undone if you join a game; to permanently save your changes, disconnect from the lobby after using the command.
|
||||
|
||||
You can see basic information about a character saved on the server (without affecting your current character) by using `$checkchar <slot>`. You can delete a previously-saved character with `$deletechar <slot>`.
|
||||
|
||||
There is also the command `$bbchar <username> <password> <slot>`, which behaves similarly to `$savechar` but writes the character data to a BB character slot in a different account instead (slots are numbered 1 through 4). This can be used to "upgrade" a character to BB from an earlier version.
|
||||
|
||||
Exactly which data is saved and loaded depends on the game version:
|
||||
|
||||
| Game | Inventory | Character | Options/chats | Quest flags | Bank | Battle/Challenge |
|
||||
|----------------------|-----------|-----------|---------------|-------------|------|------------------|
|
||||
| PSO DC v1 prototypes | Yes | Yes | No | No | No | N/A |
|
||||
| PSO DC v1 | Yes | Yes | No | No | No | N/A |
|
||||
| PSO DC v2 | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| PSO PC (v2) | Yes | Yes | No | No | No | Save only |
|
||||
| PSO GC NTE | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| PSO GC (not Plus) | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| PSO GC Plus (1) | Save only | Save only | No | No | No | Save only |
|
||||
| PSO GC Ep3 (1) | No | Save only | No | No | No | Save only |
|
||||
| PSO Xbox | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| PSO BB | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
|
||||
*Notes*:
|
||||
1. *If EnableSendFunctionCallQuestNumber is enabled in config.json, then $savechar and $loadchar can save and restore all character data on these versions, just like on GC non-Plus. Episode 3 characters exist in a separate namespace; that is, you can't use $savechar and $loadchar to convert an Ep3 character to non-Ep3, or vice versa.*
|
||||
|
||||
## Episode 3 features
|
||||
|
||||
newserv supports many features unique to Episode 3:
|
||||
* CARD battles. Not every combination of abilities has been tested yet, so if you find a feature or card ability that doesn't work like it's supposed to, please make a GitHub issue and describe the situation (the attacking card(s), defending card(s), and ability or condition that didn't work).
|
||||
* Spectator teams.
|
||||
* Tournaments. (But they work differently than Sega's tournaments did - see below)
|
||||
* Downloading quests.
|
||||
* Trading cards.
|
||||
* Participating in card auctions. (The auction contents must be configured in config.json.)
|
||||
* Decorations in lobbies. Currently only images are supported; the game also supports loading custom 3D models in lobbies, but newserv does not implement this (yet).
|
||||
|
||||
### Battle records
|
||||
|
||||
After playing a battle, you can save the record of the battle with the `$saverec` command. You can then replay the battle later by using the `$playrec` command in a lobby - this will create a spectator team and play the recording of the battle as if it were happening in realtime. Note that there is a bug in older versions of Dolphin that seems to be frequently triggered when playing battle records, which causes the emulator to crash with the message `QObject::~QObject: Timers cannot be stopped from another thread`. To avoid this, use the latest version of Dolphin.
|
||||
|
||||
### Tournaments
|
||||
|
||||
Tournaments work differently than they did on Sega's servers. Tournaments can be created with the `create-tournament` shell command, which enables players to register for them. (Use `help` to see all the arguments - there are many!) The `start-tournament` shell command starts the tournament (and prevents further registrations), but this doesn't schedule any matches. Instead, players who are ready to play their next match can all stand at the 4-player battle table near the lobby warp in the same CARD lobby, and the tournament match will start automatically.
|
||||
|
||||
These tournament semantics mean that there can be multiple matches in the same tournament in play simultaneously, and not all matches in a round must be complete before the next round can begin - only the matches preceding each individual match must be complete for that match to be playable.
|
||||
|
||||
The Meseta rewards for winning tournament matches can be configured in config.json.
|
||||
|
||||
### Episode 3 files
|
||||
|
||||
Episode 3 state and game data is stored in the system/ep3 directory. The files in there are:
|
||||
* card-definitions.mnr: Compressed card definition list, sent to Episode 3 clients at connect time. Card stats and abilities can be changed by editing this file.
|
||||
* card-definitions.mnrd: Decompressed version of the above. If present, newserv will use this instead of the compressed version, since this is easier to edit.
|
||||
* card-text.mnr: Compressed card text archive. Generally only used for debugging.
|
||||
* card-text.mnrd: Decompressed card text archive; same format as TextCardE.bin. Generally only used for debugging.
|
||||
* com-decks.json: COM decks used in tournaments. The default decks in this file come from logs from Sega's servers, so the file doesn't include every COM deck Sega ever made - the rest are probably lost to time.
|
||||
* maps/: Online free battle and quest maps (.mnm/.bin/.mnmd/.bind files). newserv comes with the default online maps, as well as some fan-made variations and quests to help new players get up to speed. Within the maps/ directory, each subdirectory is treated as a separate category and may be optionally downloadable or available at the battle setup counter. The category.json file in each subdirectory specifies the category's behavior; see system/ep3/maps/online/category.json for a documented example.
|
||||
* tournament-state.json: State of all active tournaments. This file is automatically written when any tournament changes state for any reason (e.g. a tournament is created/started/deleted or a match is resolved).
|
||||
|
||||
There is no public editor for Episode 3 maps and quests, but the format is described fairly thoroughly in src/Episode3/DataIndexes.hh (see the MapDefinition structure). You'll need to use `newserv decompress-prs ...` to decompress a .bin or .mnm file before editing it, but you don't need to compress it again to use it - just put the .bind or .mnmd file in the maps directory and newserv will make it available.
|
||||
|
||||
Like quests, Episode 3 card definitions, maps, and quests are cached in memory. If you've changed any of these files, you can run `reload ep3-cards` or `reload ep3-maps` in the interactive shell to make the changes take effect without restarting the server.
|
||||
|
||||
## Memory patches and client functions
|
||||
|
||||
You can put assembly files in the system/client-functions directory with filenames like PatchName.VERS.patch.s and they will appear in the Patches menu for clients that support client functions. Client functions are written in SH-4, PowerPC, or x86 assembly and are compiled when newserv is started. The assembly system's features are documented in the comments in system/client-functions/System/WriteMemoryGC.ppc.s.
|
||||
|
||||
The VERS token in client function filenames refers to the specific version of the game that the client function applies to. Some versions do not support receiving client functions at all. *Note: newserv uses the shorter GameCube versioning convention, where discs labeled DOL-XXXX-0-0Y are version 1.Y. The PSO community seems to use the convention 1.0Y in some places instead, but these are the same version. For example, the version that newserv calls v1.4 is the same as v1.04, and is labeled DOL-GPOJ-0-04 on the underside of the disc.*
|
||||
|
||||
The specific versions are:
|
||||
|
||||
| Game | VERS | CPU architecture |
|
||||
|------------------------------|------|--------------------------------|
|
||||
| PSO DC Network Trial Edition | 1OJ1 | Client functions not supported |
|
||||
| PSO DC 11/2000 prototype | 1OJ2 | Client functions not supported |
|
||||
| PSO DC 12/2000 prototype | 1OJ3 | Client functions not supported |
|
||||
| PSO DC 01/2001 prototype | 1OJ4 | Client functions not supported |
|
||||
| PSO DC v1 JP | 1OJF | Client functions not supported |
|
||||
| PSO DC v1 US | 1OEF | Client functions not supported |
|
||||
| PSO DC v1 EU | 1OPF | Client functions not supported |
|
||||
| PSO DC 08/06/2001 prototype | 2OJ4 | SH-4 |
|
||||
| PSO DC 08/22/2001 prototype | 2OJ5 | SH-4 |
|
||||
| PSO DC v2 JP | 2OJF | SH-4 |
|
||||
| PSO DC v2 US | 2OEF | SH-4 |
|
||||
| PSO DC v2 EU | 2OPF | SH-4 |
|
||||
| PSO PC (v2) Trial Edition | 2OJT | Client functions not supported |
|
||||
| PSO PC (v2) 04/2002 | 2OJW | Client functions not supported |
|
||||
| PSO PC (v2) 02/2003 | 2OJZ | Client functions not supported |
|
||||
| PSO GC Trial Edition | 3OJT | PowerPC |
|
||||
| PSO GC v1.2 JP | 3OJ2 | PowerPC |
|
||||
| PSO GC v1.3 JP | 3OJ3 | PowerPC |
|
||||
| PSO GC v1.4 (Plus) JP | 3OJ4 | PowerPC |
|
||||
| PSO GC v1.5 (Plus) JP | 3OJ5 | PowerPC (1) |
|
||||
| PSO GC v1.0 US | 3OE0 | PowerPC |
|
||||
| PSO GC v1.1 US | 3OE1 | PowerPC |
|
||||
| PSO GC v1.2 (Plus) US | 3OE2 | PowerPC (1) |
|
||||
| PSO GC v1.0 EU | 3OP0 | PowerPC |
|
||||
| PSO GC Ep3 Trial Edition | 3SJT | PowerPC |
|
||||
| PSO GC Ep3 JP | 3SJ0 | PowerPC |
|
||||
| PSO GC Ep3 US | 3SE0 | PowerPC (1) |
|
||||
| PSO GC Ep3 EU | 3SP0 | PowerPC (1) |
|
||||
| PSO Xbox Beta | 4OJB | x86 |
|
||||
| PSO Xbox JP Disc | 4OJD | x86 |
|
||||
| PSO Xbox JP TU | 4OJU | x86 |
|
||||
| PSO Xbox US Disc | 4OED | x86 |
|
||||
| PSO Xbox US TU | 4OEU | x86 |
|
||||
| PSO Xbox EU Disc | 4OPD | x86 |
|
||||
| PSO Xbox EU TU | 4OPU | x86 |
|
||||
| PSO BB JP 1.25.11 | 59NJ | x86 |
|
||||
| PSO BB JP 1.25.13 | 59NL | x86 |
|
||||
| PSO BB Tethealla | 59NL | x86 |
|
||||
|
||||
*Notes:*
|
||||
1. *Client functions are only supported on these versions if EnableSendFunctionCallQuestNumbers is set in config.json. See the comments there for more information.*
|
||||
|
||||
newserv comes with a set of patches for many of the above versions. These are organized in subdirectories within system/client-functions/.
|
||||
|
||||
### DOL loader
|
||||
|
||||
You can put DOL files in the system/dol directory, and they will appear in the Programs menu for GC clients. Selecting a DOL file there will load the file into the GameCube's memory and run it, just like the old homebrew loaders (PSUL and PSOload) did. For this to work, ReadMemoryWordGC.ppc.s, WriteMemoryGC.ppc.s, and RunDOL.ppc.s must be present in the system/client-functions/System directory. This has been tested on Dolphin but not on a real GameCube, so results may vary.
|
||||
|
||||
Like other kinds of data, functions and DOL files are cached in memory. If you've changed any of these files, you can run `reload functions` or `reload dol-files` in the interactive shell to make the changes take effect without restarting the server.
|
||||
|
||||
I mainly built the DOL loading functionality for documentation purposes. By now, there are many better ways to load homebrew code on an unmodified GameCube, but to my knowledge there isn't another open-source implementation of this method in existence.
|
||||
|
||||
## Using newserv as a proxy
|
||||
|
||||
If you want to play online on remote servers rather than running your own server, newserv also includes a PSO proxy. Currently this works with PSO GC and may work with PC and DC; it also works with some BB clients in specific situations.
|
||||
|
||||
To use the proxy for PSO DC, PC, or GC, add an entry to the corresponding ProxyDestinations dictionary in config.json, then run newserv and connect to it as normal (see below). You'll see a "Proxy server" option in the main menu, and you can pick which remote server to connect to.
|
||||
|
||||
To use the proxy for PSO BB, set the ProxyDestination-BB entry in config.json. If this option is set, it essentially disables the game server for all BB clients - all BB clients will be proxied to the specified destination instead. Unfortunately, because PSO BB uses a different set of handlers for the data server phase and character selection, there's no in-game way to present the player with a list of options, like there is on PSO PC and PSO GC.
|
||||
|
||||
When you're on PSO DC, PC, GC, or Xbox and are connected to a remote server through newserv's proxy, choosing the Change Ship or Change Block action from the lobby counter will send you back to newserv's main menu instead of the remote server's ship or block select menu. You can go back to the server you were just on by choosing it from the proxy server menu again.
|
||||
|
||||
There are many options available when starting a proxy session. All options are off by default unless otherwise noted. The options are:
|
||||
* **Chat commands**: enables chat commands in the proxy session (on by default).
|
||||
* **Chat filter**: enables escape sequences in chat messages and info board (on by default).
|
||||
* **Player notifications**: shows a message when any player joins or leaves the game or lobby you're in.
|
||||
* **Block pings**: blocks automatic pings sent by the client, and responds to ping commands from the server automatically.
|
||||
* **Infinite HP**: automatically heals you whenever you get hit. An attack that kills you in one hit will still kill you, however.
|
||||
* **Infinite TP**: automatically restores your TP whenever you use any technique.
|
||||
* **Switch assist**: unlocks doors that require two or four players in a one-player game, when you step on one of the switches.
|
||||
* **Infinite Meseta** (Episode 3 only): gives you 1,000,000 Meseta, regardless of the value sent by the remote server.
|
||||
* **Block events**: disables holiday events sent by the remote server.
|
||||
* **Block patches**: prevents any B2 (patch) commands from reaching the client.
|
||||
* **Save files**: saves copies of several kinds of files when they're sent by the remote server. The files are written to the current directory (which is usually the directory containing the system/ directory). Saved files can then be used with newserv by just moving the file into the appropriate place in the system/ directory and renaming it appropriately. These kinds of files can be saved:
|
||||
* Online quests and download quests (saved as .bin/.dat files)
|
||||
* GBA games (saved as .gba files)
|
||||
* Patches (saved as .bin files and disassembled as .txt files)
|
||||
* Player, system, and Guild Card data from BB sessions (saved as .psochar, .psosys, .psosysteam, and .psocard files)
|
||||
* Stream file data from BB sessions (saved as ItemPMT, BattleParamEntry, ItemMagEdit, and PlyLevelTbl files)
|
||||
* Episode 3 online quests and maps (saved as .mnmd files)
|
||||
* Episode 3 download quests (saved as .mnm files)
|
||||
* Episode 3 card definitions (saved as .mnr files)
|
||||
* Episode 3 media updates (saved as .gvm, .bml, or .bin files)
|
||||
|
||||
The remote server will probably try to assign you a Guild Card number that doesn't match the one you have on newserv. The proxy rewrites the commands in transit to make it look like the remote server assigned you the same Guild Card number as you have on newserv, but if the remote server has some external integrations (e.g. forum or Discord bots), they will use the Guild Card number that the remote server believes it has assigned to you. The number assigned by the remote server is shown to you when you first connect to the remote server, and you can retrieve it in lobbies or during games with the `$li` command.
|
||||
|
||||
Some chat commands (see below) have the same basic function on the proxy but have different effects or conditions. In addition, there are some server shell commands that affect clients on the proxy (run `help` in the shell to see what they are). If there's only one proxy session open, the shell's proxy commands will affect that session. Otherwise, you'll have to specify which session to affect with the `on` prefix - to send a chat message in C-17's session, for example, you would run `on C-17 chat ...`.
|
||||
|
||||
## Chat commands
|
||||
|
||||
newserv supports a variety of commands players can use by chatting in-game. Any chat message that begins with `$` is treated as a chat command. (If you actually want to send a chat message starting with `$`, type `$$` instead.) On the DC 11/2000 prototype, `@` is used instead of `$` for all chat commands, since `$` does not appear on the English virtual keyboard.
|
||||
|
||||
Some commands only work for clients not in proxy sessions. The chat commands are:
|
||||
|
||||
* Information commands
|
||||
* `$li`: Show basic information about the lobby or game you're in. If you're on the proxy, show information about your connection instead (remote Guild Card number, client ID, etc.).
|
||||
* `$si`: Show basic information about the server.
|
||||
* `$ping`: Show round-trip ping time from the server to you. On the proxy, show the ping time from you to the proxy and from the proxy to the server.
|
||||
* `$matcount` (non-proxy only): Show how many of each type of material you've used.
|
||||
* `$killcount` (non-proxy only): Show the kill count on your currently-equipped weapon. If you're in a game and not on BB, the value is only accurate at the time the item enters the game.
|
||||
* `$itemnotifs <mode>`: Enable item drop notification messages. If the game has private drops enabled, you will only see a notification if the dropped item is visible to you; you won't be notified of other players' drops. The modes are:
|
||||
* `off`: No notifications are shown.
|
||||
* `rare`: You are notified when a rare item drops.
|
||||
* `on`: You are notified when any item drops, except Meseta.
|
||||
* `every`: You are notified when any item drops, including Meseta.
|
||||
* `$announcerares`: Enable or disable announcements for your rare item finds. This determines whether rare items you find will be announced to the game and server, not whether you will see announcements for others finding rare items.
|
||||
* `$what` (non-proxy only): Show the type, name, and stats of the nearest item on the ground.
|
||||
* `$where`: Show your current floor number and coordinates. Mainly useful for debugging.
|
||||
* `$qfread <field-name>` (non-proxy only): Show the value of a quest counter in your player data. The field names are defined in config.json.
|
||||
|
||||
* Basic debugging commands (special permissions not required)
|
||||
* `$whatobj` and `$whatene` (non-proxy only): Tells you what the closest object or enemy spawn point is to your position, along with its coordinates and object or enemy ID. The full definition is also printed to the server's log.
|
||||
* `$qcheck <flag-num>` (non-proxy only): Show the value of a quest flag. If you're in a game, show the value of the flag in that game; if you're in the lobby, show the saved value of that quest flag for your character (BB only).
|
||||
* `$qgread <flag-num>` (non-proxy only): Show the value of a quest counter ("global flag").
|
||||
* `$sound <sound-id>`: Play the given sound (GC only).
|
||||
|
||||
* Restricted debugging commands (`$debug` permission required)
|
||||
* `$debug`: Enable debug mode. You need the DEBUG flag in your user account to use this command. Enabling debug does several things:
|
||||
* You'll be able to use the rest of the commands in this section.
|
||||
* You'll see in-game messages from the server when you take some actions, like killing enemies, opening boxes, or flipping switches.
|
||||
* You'll see the rare seed value and floor variations when you join a game.
|
||||
* You'll be placed into the last available slot in lobbies and games instead of the first, unless you're joining a BB solo-mode game.
|
||||
* You'll be able to join games with any PSO version, not only those for which cross-version play is normally enabled. See the "Cross-version play" section above for details on this.
|
||||
* `$readmem <address>`: Read 4 bytes from the given address and show you the values.
|
||||
* `$writemem <address> <data>`: Write data to the given address. Data is not required to be any specific size.
|
||||
* `$nativecall <address> [arg1 ...]` (GC only): Call a native function on your client. Only arguments passed in registers are supported; calling functions that take many arguments is not supported.
|
||||
* `$quest <number>` (non-proxy only): Load a quest by quest number. Can be used to load battle or challenge quests with only one player present. `$debug` is not required for this command if the specified quest has the AllowStartFromChatCommand field set in its metadata file.
|
||||
* `$qcall <function-id>`: Call a quest function on your client.
|
||||
* `$qset <flag-num>` or `$qclear <flag-num>`: Set or clear a quest flag for everyone in the game. If you're in the lobby and on BB, set or clear the saved value of a quest flag in your character file.
|
||||
* `$qgwrite <flag-num> <value>` (non-proxy only): Set the value of a quest counter ("global flag") for yourself.
|
||||
* `$qsync <reg-num> <value>`: Set a quest register's value for yourself only. `<reg-num>` should be either rXX (e.g. r60) or fXX (e.g. f60); if the latter, `<value>` is parsed as a floating-point value instead of as an integer.
|
||||
* `$qsyncall <reg-num> <value>`: Set a quest register's value for everyone in the game. `<reg-num>` should be either rXX (e.g. r60) or fXX (e.g. f60); if the latter, `<value>` is parsed as a floating-point value instead of as an integer.
|
||||
* `$swset [floor] <flag-num>` and `$swclear [floor] <flag-num>`: Set or clear a switch flag. If floor is not given, sets or clears the flag on your current floor.
|
||||
* `$swsetall`: Set all switch flags on your current floor. This unlocks all doors, disables all laser fences, triggers all light/poison switches, etc.
|
||||
* `$gc` (non-proxy only): Send your own Guild Card to yourself.
|
||||
* `$sc <data>`: Send a command to yourself.
|
||||
* `$ss <data>`: Send a command to the remote server (if in a proxy session) or to the game server.
|
||||
* `$sb <data>`: Send a command to yourself, and to the remote server or game server.
|
||||
* `$auction` (Episode 3 only): Bring up the CARD Auction menu, even if there are fewer than 4 players are in the game or you don't have a VIP card.
|
||||
* `$makeobj <type> [coords...] [angles...] [params...]`: Create a map object. This is only implemented for a few specific client versions. The type is an integer like `273` or `0x0107`. Coordinates are specified as e.g. `x:30 y:0 z:-25.5`; if coordinates are not specified, the object is created at the player's coordinates. Angles are specified as e.g. `r:0 p:0x1000 w:-0x400` (for roll, pitch, and yaw, respectively). Parameters are specified as e.g. `1:2.0 2:0.0 5:0x4000`; any unspecified parameters are set to zero. The object is only created for the calling player and is not added to the server's map state; if the object ever sends update commands (e.g. 6x0B), it will likely result in a disconnection.
|
||||
|
||||
* Personal state commands
|
||||
* `$arrow <color-id>`: Change your lobby arrow color. The color may be specified by number (0-12) or by name (red, blue, green, yellow, purple, cyan, orange, pink, white, white2, white3, or black).
|
||||
* `$secid <section-id>`: Set your override section ID. After running this command, any games you create will use your override section ID for rare drops instead of your character's actual section ID. If you're in a game and you are the leader of the game, this also immediately changes the item tables used by the server when creating items. To revert to your actual section id, run `$secid` with no name after it. On the proxy, this will not work if the remote server controls item drops. If the server does not allow cheat mode anywhere (that is, "CheatModeBehavior" is "Off" in config.json), this command does nothing.
|
||||
* `$rand <seed>`: Set your override random seed (specified as a 32-bit hex value). This will make any games you create use the given seed for rare enemies and item drops. On the proxy, this command can cause desyncs with other players in the same game, since they will not see the overridden random seed. To remove the override, run `$rand` with no arguments. If the server does not allow cheat mode anywhere (that is, "CheatModeBehavior" is "Off" in config.json), this command does nothing.
|
||||
* `$ln [name-or-type]`: Set the lobby number. Visible only to you. This command exists because some non-lobby maps can be loaded as lobbies with invalid lobby numbers. See the "GC lobby types" and "Ep3 lobby types" entries in the information menu for acceptable values here. Note that non-lobby maps do not have a lobby counter, so there's no way to exit the lobby without using either `$ln` again or `$exit`. On the game server, `$ln` reloads the lobby immediately; on the proxy, it doesn't take effect until you load another lobby yourself (which means you'll like have to use `$exit` to escape). Run this command with no argument to return to the default lobby.
|
||||
* `$swa`: Enable or disable switch assist. When enabled, the server will unlock two-player and four-player doors in non-quest games when you step on any of the required switches.
|
||||
* `$exit`: If you're in a lobby, send you to the main menu (which ends your proxy session, if you're in one). If you're in a game or spectator team, send you to the lobby (but does not end your proxy session if you're in one). Does nothing if you're in a non-Episode 3 game and no quest is in progress.
|
||||
* `$patch <name>`: Run a patch on your client. `<name>` must exactly match the name of a patch on the server.
|
||||
|
||||
* Character data commands (non-proxy only)
|
||||
* `$switchchar <slot>` (BB only): Switch to a different character from your account without logging out.
|
||||
* `$savechar <slot>`: Save your current character data on the server in the specified slot. See the [server-side saves section](#server-side-saves) for more details.
|
||||
* `$loadchar <slot>`: Load character data from the specified slot on the server, and replace your current character with it. See the [server-side saves section](#server-side-saves) for more details.
|
||||
* `$bbchar <username> <password> <slot>`: Save your current character data on the server in a different account's BB character slots. See the [server-side saves section](#server-side-saves) for more details.
|
||||
* `$checkchar [slot]`: Tells you basic information about a server-side character previously saved using `$savechar`. If `slot` is not given, tells you which slots are used and which are free.
|
||||
* `$deletechar <slot>`: Deletes a server-side character previously saved using `$savechar`.
|
||||
* `$edit <stat> <value>`: Modify your character data. See the [using $edit](#using-edit) section for details.
|
||||
|
||||
* Blue Burst player commands (non-proxy only)
|
||||
* `$bank [number]`: Switch your current bank, so you can access your other character's banks (if `number` is 1-4) or your shared account bank (if `number` is 0). If `number` is not given, switch back to your current character's bank.
|
||||
* `$save`: Save your character, system, and Guild Card data immediately. (By default, your character is saved every 60 seconds while online, and your account and Guild Card data are saved whenever they change.)
|
||||
|
||||
* Game state commands (non-proxy only)
|
||||
* `$maxlevel <level>`: Set the maximum level for players to join the current game. (This only applies when joining; if a player joins and then levels up past this level during the game, they are not kicked out, but won't be able to rejoin if they leave.)
|
||||
* `$minlevel <level>`: Set the minimum level for players to join the current game.
|
||||
* `$password <password>`: Set the game's join password. To unlock the game, run `$password` with nothing after it.
|
||||
* `$dropmode [mode]`: Change the way item drops behave in the current game. `mode` can be `none`, `client`, `shared`, `private`, or `duplicate`. If `mode` is not given, tells you the current drop mode without changing it. See the [item tables and drop modes section](#item-tables-and-drop-modes) for more information.
|
||||
* `$persist`: Enable or disable persistence for the current game. When persistence is on, the game will not be deleted when the last player leaves. The states of enemies, objects, and switches will be saved, and items left on the floor will not be deleted. (But if you're in the private or duplicate drop mode, items dropped by enemies are deleted - to make sure a certain item won't be deleted, you can pick it up and drop it again.) If the game is empty for too long (15 minutes by default), it is then deleted.
|
||||
|
||||
* Episode 3 commands (non-proxy only)
|
||||
* `$spec`: Toggle the allow spectators flag for Episode 3 games. If any players are spectating when this flag is disabled, they are sent back to the lobby.
|
||||
* `$inftime`: Toggle infinite-time mode. Must be used before starting a battle. If infinite-time mode is on, the overall and per-phase time limits will be disabled regardless of the values chosen during battle rules setup. After completing a battle, infinite-time mode is reset to the server's default value (which can be set in Episode3BehaviorFlags in config.json).
|
||||
* `$dicerange [d:L-H] [1:L-H] [a1:L-H] [d1:L-H]`: Set override dice ranges for the next battle. The min and max dice values from the rules setup menu always apply to the ATK dice, but you can specify a different range for the DEF dice with `d:2-4` (for example). The `1:` override applies to the 1-player team in a 2v1 game (so you would set the 2-player team's desired dice range in the rules menu). You can also specify the 1-player team's ATK and DEF ranges separately with the `a1:` and `d1:` overrides. Note that these ranges will only be used if the chosen map or quest does not override them.
|
||||
* `$stat <what>`: Show a statistic about your player or team in the current battle. `<what>` can be `duration`, `fcs-destroyed`, `cards-destroyed`, `damage-given`, `damage-taken`, `opp-cards-destroyed`, `own-cards-destroyed`, `move-distance`, `cards-set`, `fcs-set`, `attack-actions-set`, `techs-set`, `assists-set`, `defenses-self`, `defenses-ally`, `cards-drawn`, `max-attack-damage`, `max-combo`, `attacks-given`, `attacks-taken`, `sc-damage`, `damage-defended`, or `rank`.
|
||||
* `$surrender`: Cause your team to immediately lose the current battle. If your story character is already defeated, you can't surrender - only your teammate can.
|
||||
* `$saverec <name>`: Save the recording of the last battle.
|
||||
* `$playrec <name>`: Play a battle recording. This command creates a spectator team and plays the specified recording as if it were happening in real time. By default, playback will start immediately when the spectator team is ready; you can delay this to allow others to join by prepending a `!` to the recording name. In that case, using `$playrec` again (with no argument) within the spectator team will start playback.
|
||||
|
||||
* Cheat mode commands
|
||||
* `$cheat` (non-proxy only): Enable or disable cheat mode for the current game. All other cheat mode commands do nothing if cheat mode is disabled. By default, cheat mode is off in new games but can be enabled; there is an option in config.json that allows you to disable cheat mode entirely, or set it to on by default in new games. Cheat mode is always enabled on the proxy, unless cheat mode is disabled on the entire server.
|
||||
* `$infhp`: Enable or disable infinite HP mode. Applies to only you; does not affect other players. When enabled, one-hit KO attacks will still kill you, but on most versions of the game, the server will automatically revive you if you die. Infinite HP also automatically cures status ailments.
|
||||
* `$inftp`: Enable or disable infinite TP mode. Applies to only you; does not affect other players. Does not work on DCv1 or earlier versions.
|
||||
* `$fastkill`: Enable or disable fast kills. Applies to only you; does not affect other players. When enabled, the server will kill any enemy after you hit it once. Bosses are not affected by fast kills.
|
||||
* `$warpme <floor-id>` (or `$warp <floor-id>`): Warp yourself to the given floor.
|
||||
* `$warpall <floor-id>`: Warp everyone in the game to the given floor. You must be the leader to use this command, unless you're on the proxy.
|
||||
* `$next`: Warp yourself to the next floor.
|
||||
* `$item <desc>` (or `$i <desc>`): Create an item. `desc` may be a description of the item or a string of hex data specifying the item code. Item codes are 16 hex bytes; at least 2 bytes must be specified, and all unspecified bytes are zeroes. If you are on the proxy, you must not be using Blue Burst for this command to work. On the game server, this command works for all versions. Here are some examples to illustrate the syntax (nothing is case-sensitive, and everything except the item name itself is optional):
|
||||
* `$item Saber +5 0/10/25/0/10` (weapon with special, grind and attributes)
|
||||
* `$item ???? Draw Autogun` (untekked weapon with special; can have grind/attributes too, as above)
|
||||
* `$item SEALED J-SWORD K:2000` (weapon with kill count)
|
||||
* `$item ES APHEX ZALURE TWIN +200` (ES weapon must be prefixed with "ES"; name comes before special)
|
||||
* `$item DF FIELD +10DEF +20EVP +4` (armor with DFP bonus, EVP bonus, and slot count)
|
||||
* `$item RED MERGE +10DFP +20EVP` (shield; same as armor except without slot count)
|
||||
* `$item Knight/Power +9` (unit with specific modifier)
|
||||
* `$item Knight/Power++` (unit with normal modifier; ++/-- are +4/-4 and +/- are +2/-2)
|
||||
* `$item LIMITER K:1000` (sealed unit with kill count)
|
||||
* `$item Tapas PB:F,G,M&Y 120% 200IQ 5/195/0/0 green` (mag with PBs, synchro, IO, stats, and color)
|
||||
* `$item Trimate x10` (tool with stack size)
|
||||
* `$item Disk:Reverser` (technique disk without level)
|
||||
* `$item Disk:Razonde Lv.30` (technique disk with level)
|
||||
* `$item 1000 Meseta`
|
||||
* `$unset <index>` (non-proxy only): In an Episode 3 battle, removes one of your set cards from the field. `<index>` is the index of the set card as it appears on your screen - 1 is the card next to your SC's icon, 2 is the card to the right of 1, etc. This does not cause a Hunters-side SC to lose HP, as they normally do when their items are destroyed. You can also destroy the assist card set on yourself with `$unset 0`.
|
||||
* `$dropmode [mode]` (proxy only): Change the way item drops behave in the current game, if you are not on BB. Unlike the game server version of this command, using this on the proxy requires cheats to be enabled. This works by intercepting the drop requests sent to and from the leader. (So, if you are the leader and not using server drop mode on the remote server, it affects the entire game; otherwise, it affects only items generated by your actions.) `mode` can be `none` (no drops), `default` (normal drops), or `proxy` (use newserv's drop tables instead of the remote server's). If `mode` is not given, tells you the current drop mode without changing it.
|
||||
|
||||
* Aesthetic commands
|
||||
* `$event <event>`: Set the current holiday event in the current lobby. Holiday events are documented in the "Using $event" item in the information menu. If you're on the proxy, this applies to all lobbies and games you join, but only you will see the new event - other players will not.
|
||||
* `$allevent <event>` (non-proxy only): Set the current holiday event in all lobbies.
|
||||
* `$song <song-id>` (Episode 3 only): Play a specific song in the current lobby.
|
||||
|
||||
* Administration commands (non-proxy only)
|
||||
* `$ann <message>`: Send an announcement message. The message is sent as temporary on-screen text to all players in all games and lobbies. On BB, the message appears in the scrolling top bar.
|
||||
* `$ann!`, `$ann?`, `$ann?!`: Same as `$ann`, but with `?`, omits the sender's name, and with `!`, sends the message as a Simple Mail message instead of on-screen text.
|
||||
* `$silence <identifier>`: Silence a player (remove their ability to chat) or unsilence a player. The identifier may be the player's name or Guild Card number.
|
||||
* `$kick <identifier>`: Disconnect a player. The identifier may be the player's name or Guild Card number.
|
||||
* `$ban <duration> <identifier>`: Ban a player. The duration should be of the form `10m` (minutes), `10h` (hours), `10d` (days), `10w` (weeks), `10M` (months), or `10y` (years). (Numbers other than 10 may be used, of course.) As with `$kick`, the identifier may be the player's name or Guild Card number.
|
||||
|
||||
### Using $edit
|
||||
|
||||
The $edit command modifies your character data. This command doesn't work on V3 (GameCube/Xbox). If you are on V1 or V2 (DC or PC, not BB), your changes will be undone if you join a game - to save your changes, disconnect from the lobby.
|
||||
|
||||
Some subcommands are always available. They are:
|
||||
* `$edit mat reset power`: Clear your usage of power materials (BB only)
|
||||
* `$edit mat reset mind`: Clear your usage of mind materials (BB only)
|
||||
* `$edit mat reset evade`: Clear your usage of evade materials (BB only)
|
||||
* `$edit mat reset def`: Clear your usage of def materials (BB only)
|
||||
* `$edit mat reset luck`: Clear your usage of luck materials (BB only)
|
||||
* `$edit mat reset hp`: Clear your usage of HP materials (BB only)
|
||||
* `$edit mat reset tp`: Clear your usage of TP materials (BB only)
|
||||
* `$edit mat reset all`: Clear your usage of all materials except HP and TP (BB only)
|
||||
* `$edit mat reset every`: Clear your usage of all materials including HP and TP (BB only)
|
||||
* `$edit namecolor AARRGGBB`: Set your name color (AARRGGBB specified in hex)
|
||||
* `$edit language L`: Set your language (Generally only useful on BB; values for L: J = Japanese, E = English, G = German, F = French, S = Spanish, B = Simplified Chinese, T = Traditional Chinese, K = Korean)
|
||||
* `$edit name NAME`: Set your character name
|
||||
* `$edit npc NPC-NAME`: Set or remove an NPC skin on your character (use `none` to remove a skin). The NPC names are:
|
||||
* On all versions except DCv1 and early prototypes: `ninja`, `rico`, `sonic`, `knuckles`, `tails`
|
||||
* On GC, Xbox, and BB: `flowen`, `elly`
|
||||
* On BB only: `momoka`, `irene`, `guild`, `nurse`
|
||||
* `$edit secid SECID-NAME`: Set your section ID (cheat mode is required unless your character is Level 1)
|
||||
|
||||
The remaining subcommands are only available if cheat mode is enabled on the server. They are:
|
||||
* `$edit atp N`: Set your ATP to N until stats are updated (e.g. by leveling up)
|
||||
* `$edit mst N`: Set your MST to N until stats are updated
|
||||
* `$edit evp N`: Set your EVP to N until stats are updated
|
||||
* `$edit dfp N`: Set your DFP to N until stats are updated
|
||||
* `$edit ata N`: Set your ATA to N until stats are updated
|
||||
* `$edit lck N`: Set your LCK to N until stats are updated
|
||||
* `$edit hp N`: Set your HP to N until stats are updated
|
||||
* `$edit meseta N`: Set the amount of Meseta in your inventory
|
||||
* `$edit exp N`: Set your total amount of EXP (does not affect level)
|
||||
* `$edit level N`: Set your current level (recomputes stats, but does not affect EXP)
|
||||
* `$edit tech TECH-NAME LEVEL`: Set the level of one of your techniques
|
||||
|
||||
## REST API
|
||||
|
||||
newserv has an optional HTTP server that provides a way to programmatically get data from the server in realtime. This is intended for use with external integrations; for example, a web site could query this API to get the current player count to display on the home page.
|
||||
|
||||
The HTTP server is disabled by default, and you have to explicitly enable it in config.json if you want this functionality. **If you enable it, make sure that the HTTP port can't be accessed from the public Internet.** The API provides a lot of internal data about players and games, and it should only be accessed by programs that you've written or that you trust.
|
||||
|
||||
To enable the HTTP server, add a port number in the HTTPListen list in config.json. The HTTP server will listen on that port.
|
||||
|
||||
All returned data is JSON-encoded, and all request data (for POST requests) must also be JSON-encoded with the `Content-Type: application/json` header.
|
||||
|
||||
The HTTP server has the following endpoints:
|
||||
* `GET /`: Returns the server's build date and revision.
|
||||
* `GET /y/data/ep3-cards`: Returns the Episode 3 card definitions.
|
||||
* `GET /y/data/ep3-cards-trial`: Returns the Episode 3 Trial Edition card definitions.
|
||||
* `GET /y/data/common-tables`: Returns the parameters for generating common items (ItemPT files). This endpoint returns a lot of data and can be slow!
|
||||
* `GET /y/data/rare-tables`: Returns a list of rare table names.
|
||||
* `GET /y/data/rare-tables/<TABLE-NAME>` (for example, `/y/data/rare-tables/rare-table-v4`): Returns the contents of a rare item table.
|
||||
* `GET /y/data/quests`: Returns metadata about all available quests and quest categories.
|
||||
* `GET /y/data/config`: Returns the server's configuration file.
|
||||
* `GET /y/accounts`: Returns information about all registered accounts.
|
||||
* `GET /y/clients`: Returns information about all connected clients on the game server.
|
||||
* `GET /y/proxy-clients`: Returns information about all connected clients on the proxy.
|
||||
* `GET /y/lobbies`: Returns information about all lobbies and games.
|
||||
* `GET /y/server`: Returns information about the server.
|
||||
* `GET /y/summary`: Returns a summary of the server's state, connected clients, active games, and proxy sessions.
|
||||
* `WS /y/rare-drops/stream`: WebSocket endpoint that sends messages whenever an announceable rare item is dropped in any game. See below.
|
||||
* `POST /y/shell-exec`: Runs a server shell command. Input should be a JSON dict of e.g. `{"command": "announce hello"}`; response will be a JSON dict of `{"result": "<result text>"}` or an HTTP error.
|
||||
|
||||
### Rare drop stream endpoint
|
||||
|
||||
The `/y/rare-drops/stream` endpoint provides a way to implement a drop log in e.g. Discord. For every announceable rare item, a message is sent to all connected clients on this endpoint. (Announceable rare items are items for which an in-game or server-wide text message is sent announcing the find.)
|
||||
|
||||
Upon connecting, you'll get the message `{"ServerType": "newserv"}`. After that, when a rare item announcement is sent, you'll get a message like this:
|
||||
```
|
||||
{
|
||||
"PlayerAccountID", 12345,
|
||||
"PlayerName", "SONIC",
|
||||
"PlayerVersion", "GC_V3",
|
||||
"GameName", "ttf",
|
||||
"GameDropMode", "SERVER_PRIVATE",
|
||||
"ItemData", "03000000 00010000 00000000 (0021002C) 00000000",
|
||||
"ItemDescription", "Monomate x1",
|
||||
"NotifyGame", true,
|
||||
"NotifyServer", false,
|
||||
}
|
||||
```
|
||||
|
||||
# Non-server features
|
||||
|
||||
newserv has many CLI options, which can be used to access functionality other than the game server and proxy. Run `newserv help` to see a full list of the options and how to use each one.
|
||||
|
||||
The data formats that newserv can convert to/from are:
|
||||
|
||||
| Format | Encode/compress action | Decode/extract action |
|
||||
|-------------------------------------|---------------------------|------------------------------|
|
||||
| PRS compression | `compress-prs` | `decompress-prs` |
|
||||
| PR2/PRC compression | `compress-pr2` | `decompress-pr2` |
|
||||
| BC0 compression | `compress-bc0` | `decompress-bc0` |
|
||||
| Raw encrypted data | `encrypt-data` | `decrypt-data` |
|
||||
| Episode 3 command mask | `encrypt-trivial-data` | `decrypt-trivial-data` |
|
||||
| Challenge Mode rank text | `encrypt-challenge-data` | `decrypt-challenge-data` |
|
||||
| PSO DC quest file (.vms) | None | `decode-vms` |
|
||||
| PSO GC quest file (.gci) | None | `decode-gci` |
|
||||
| Download quest file (.dlq) | None | `decode-dlq` |
|
||||
| Server quest file (.qst) | `encode-qst` | `decode-qst` |
|
||||
| PSO DC save file (.vms) | `encrypt-vms-save` | `decrypt-vms-save` |
|
||||
| PSO PC save file | `encrypt-pc-save` | `decrypt-pc-save` |
|
||||
| PSO GC save file (.gci) | `encrypt-gci-save` | `decrypt-gci-save` |
|
||||
| PSO Xbox save file | None | `decrypt-xbox-save` |
|
||||
| PSO GC snapshot file | None | `decode-gci-snapshot` |
|
||||
| Quest script (.bin) | `assemble-quest-script` | `disassemble-quest-script` |
|
||||
| Quest map (.dat) | None | `disassemble-quest-map` |
|
||||
| AFS archive (.afs) | None | `extract-afs` |
|
||||
| BML archive (.bml) | None | `extract-bml` |
|
||||
| PPK archive (.ppk) | None | `extract-ppk` |
|
||||
| GSL archive (.gsl) | `generate-gsl` | `extract-gsl` |
|
||||
| GVM texture (.gvm) | `encode-gvm` | None |
|
||||
| Bitmap font (.fon) | `encode-bitmap-font` | `decode-bitmap-font` |
|
||||
| Text archive | `encode-text-archive` | `decode-text-archive` |
|
||||
| Unicode text set | `encode-unicode-text-set` | `decode-unicode-text-set` |
|
||||
| Word Select data set | None | `decode-word-select-set` |
|
||||
| Set data table | None | `disassemble-set-data-table` |
|
||||
| Rare item table (AFS/GSL/JSON/HTML) | `convert-rare-item-set` | `convert-rare-item-set` |
|
||||
|
||||
There are several actions that don't fit well into the table above, which let you do other things:
|
||||
|
||||
* Compute the decompressed size of compressed PRS data without decompressing it (`prs-size`)
|
||||
* Find the likely round1 or round2 seed for a corrupt save file (`salvage-gci`)
|
||||
* Run a brute-force search for a decryption seed (`find-decryption-seed`)
|
||||
* Format Episode 3 game data in a human-readable manner (`show-ep3-maps`, `show-ep3-cards`, `generate-ep3-cards-html`)
|
||||
* Format Blue Burst battle parameter files in a human-readable manner (`show-battle-params`)
|
||||
* Convert item data to a human-readable description, or vice versa (`describe-item`)
|
||||
* Show the server's item and level tables (`show-item-tables`, `show-level-tables`)
|
||||
* Connect to another PSO server and pretend to be a client (`cat-client`)
|
||||
* Generate or describe DC serial numbers (`generate-dc-serial-number`, `inspect-dc-serial-number`)
|
||||
|
||||
# Docker
|
||||
Docker is new and mostly unsupported at this time. However, here are some best-effort steps to build and run in a docker container on Ubuntu Linux.
|
||||
Tested on Ubuntu 22.04.4 LTS.
|
||||
Note: You cannot have anything except this docker container using port 53 (DNS) on your server.
|
||||
|
||||
Install prerequisites
|
||||
```
|
||||
sudo apt install -y git
|
||||
sudo apt install -y cmake. ## minimum version is 3.10. Check installed version with "cmake --version"
|
||||
```
|
||||
|
||||
Clone repository
|
||||
```
|
||||
cd ~
|
||||
git clone https://github.com/fuzziqersoftware/newserv/
|
||||
cd ~/newserv
|
||||
```
|
||||
|
||||
Build newserv. This will take a while. Don't forget the period at the end!
|
||||
```
|
||||
sudo docker build -t newserv .
|
||||
```
|
||||
|
||||
Create persistent directories. Assuming you want to store the persistent data in your home directory
|
||||
```
|
||||
mkdir ~/newservPersist
|
||||
mkdir ~/newservPersist/players
|
||||
mkdir ~/newservPersist/teams
|
||||
mkdir ~/newservPersist/licenses
|
||||
```
|
||||
|
||||
Copy config file to config dir
|
||||
```
|
||||
cp ~/newserv/system/config.example.json ~/newservPersist/config.json
|
||||
```
|
||||
|
||||
Edit config.json
|
||||
```
|
||||
nano ~/newservPersist/config.json
|
||||
```
|
||||
Pro tip:
|
||||
Set "LocalAddress" to the static, LAN IP address of your server. If your server LAN IP is "192.168.0.10":
|
||||
"LocalAddress": "192.168.0.10",
|
||||
|
||||
Set "ExternalAddress" to the WAN IP address of your network. If your WAN IP is "8.8.8.8":
|
||||
"ExternalAddress": "8.8.8.8",
|
||||
|
||||
For Dolphin > Settings. Set SP1 to "Broadband Adapter (HLE)" Click [...] next to this, and set the DNS to the IP address of your server. Then start the game. Changes will not take affect if the game is running.
|
||||
|
||||
Docker run. Remember to change /home/changeme/newservPersist to your persistent directory. Do not use aliases such as '~'
|
||||
```
|
||||
docker run --name newserv -p 53:53/udp -p 5100:5100 -p 5110:5110 -p 5111:5111 -p 5112:5112 -p 9064:9064 -p 9100:9100 -p 9103:9103 -p 9300:9300 -p 11000:11000 -p 12000:12000 -p 12004:12004 -p 12005:12005 -v /etc/localtime:/etc/localtime:ro -v /home/changeme/newservPersist/config.json:/newserv/system/config.json -v /home/changeme/newservPersist/players:/newserv/system/players -v /home/changeme/newservPersist/teams:/newserv/system/teams -v /home/changeme/newservPersist/licenses:/newserv/system/licenses --restart no newserv:latest
|
||||
```
|
||||
|
||||
Docker run host network mode. Remember to change /home/changeme/newservPersist to your persistent directory. Do not use aliases such as '~'
|
||||
```
|
||||
docker run --net host --name newserv -v /etc/localtime:/etc/localtime:ro -v /home/changeme/newservPersist/config.json:/newserv/system/config.json -v /home/changeme/newservPersist/players:/newserv/system/players -v /home/changeme/newservPersist/teams:/newserv/system/teams -v /home/changeme/newservPersist/licenses:/newserv/system/licenses --restart no newserv:latest
|
||||
```
|
||||
|
||||
Docker compose. Remember to change /home/changeme/newservPersist to your persistent directory. Do not use aliases such as '~'
|
||||
```
|
||||
name: psonewserv
|
||||
services:
|
||||
newserv:
|
||||
container_name: newserv
|
||||
ports:
|
||||
- 53:53/udp
|
||||
- 5100:5100
|
||||
- 5110:5110
|
||||
- 5111:5111
|
||||
- 5112:5112
|
||||
- 9064:9064
|
||||
- 9100:9100
|
||||
- 9103:9103
|
||||
- 9300:9300
|
||||
- 11000:11000
|
||||
- 12000:12000
|
||||
- 12004:12004
|
||||
- 12005:12005
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /home/changeme/newservPersist/config.json:/newserv/system/config.json
|
||||
- /home/changeme/newservPersist/players:/newserv/system/players
|
||||
- /home/changeme/newservPersist/teams:/newserv/system/teams
|
||||
- /home/changeme/newservPersist/licenses:/newserv/system/licenses
|
||||
restart: no ## Set to whatever you want.
|
||||
image: newserv:latest
|
||||
```
|
||||
Docker compose host network mode. Remember to change /home/changeme/newservPersist to your persistent directory. Do not use aliases such as '~'
|
||||
```
|
||||
name: psonewserv
|
||||
services:
|
||||
newserv:
|
||||
container_name: newserv
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /home/changeme/newservPersist/config.json:/newserv/system/config.json
|
||||
- /home/changeme/newservPersist/players:/newserv/system/players
|
||||
- /home/changeme/newservPersist/teams:/newserv/system/teams
|
||||
- /home/changeme/newservPersist/licenses:/newserv/system/licenses
|
||||
restart: no ## Set to whatever you want.
|
||||
network_mode: host
|
||||
image: newserv:latest
|
||||
```
|
||||
See `LICENSE` for license details. Any copied or modified upstream code retains the original license attribution.
|
||||
|
||||
@@ -30,8 +30,6 @@
|
||||
|
||||
## PSOBB
|
||||
|
||||
- Figure out why Pouilly Slime EXP doesn't work
|
||||
- Make server-specified rare enemies work with maps loaded by the proxy
|
||||
- Implement serialization for various table types (ItemPMT, ItemPT, etc.)
|
||||
- Record some BB tests
|
||||
- Add all necessary Guild Card number rewrites in BB commands on the proxy
|
||||
|
||||
@@ -0,0 +1,926 @@
|
||||
# newserv <img align="right" src="static/s-newserv.png" />
|
||||
|
||||
newserv is a game server, proxy, and reverse-engineering tool for Phantasy Star Online (PSO). **To quickly get started using newserv, just read the [server setup](#server-setup) and [how to connect](#how-to-connect) sections.**
|
||||
|
||||
This project includes code that was reverse-engineered by the community in ages long past, and has been included in many projects since then. It also includes some game data from Phantasy Star Online itself, which was originally created by Sega.
|
||||
|
||||
Feel free to submit GitHub issues if you find bugs or have feature requests. I'd like to make the server as stable and complete as possible, but I can't promise that I'll respond to issues in a timely manner, because this is a personal project undertaken primarily for the fun of reverse-engineering. If you want to contribute to newserv yourself, pull requests are welcome as well.
|
||||
|
||||
See TODO.md for a list of known issues and future work I've curated, or go to the GitHub issue tracker for issues and requests submitted by the community.
|
||||
|
||||
**Table of contents**
|
||||
* Background
|
||||
* [History](#history)
|
||||
* [Other server projects](#other-server-projects)
|
||||
* [Using newserv in other projects](#using-newserv-in-other-projects)
|
||||
* [Contributing to newserv](#contributing-to-newserv)
|
||||
* [Compatibility](#compatibility)
|
||||
* Setup
|
||||
* [Server setup](#server-setup)
|
||||
* [Client patch directories for PC and BB](#client-patch-directories)
|
||||
* [How to connect](#how-to-connect)
|
||||
* Features and configuration
|
||||
* [User accounts](#user-accounts)
|
||||
* [Installing quests](#installing-quests)
|
||||
* [Item tables and drop modes](#item-tables-and-drop-modes)
|
||||
* [Cross-version play](#cross-version-play)
|
||||
* [Server-side saves](#server-side-saves)
|
||||
* [Episode 3 features](#episode-3-features)
|
||||
* [Memory patches, client functions, and DOL files](#memory-patches-and-client-functions)
|
||||
* [Using newserv as a proxy](#using-newserv-as-a-proxy)
|
||||
* [Chat commands](#chat-commands)
|
||||
* [REST API](#rest-api)
|
||||
* [Non-server features](#non-server-features)
|
||||
|
||||
# History
|
||||
|
||||
The history of this project essentially mirrors my development as a software engineer from the beginning of my hobby until now. If you don't care about the story, skip to the "Compatibility" or "Setup" sections below.
|
||||
|
||||
I originally purchased PSO GC when I heard about PSUL, and wanted to play around with running homebrew on my GameCube. This pathway eventually led to [GCARS-CS](https://github.com/fuzziqersoftware/gcars-cs), but that's another story.
|
||||
|
||||
<img align="left" src="static/s-khyps.png" /> After playing PSO for a while, both offline and online, I wrote a proxy called Khyps sometime in 2003. This was back in the days of the official Sega servers, where vulnerabilities weren't addressed in a timely manner or at all. It was common for malicious players using their own proxies or Action Replay codes (a story for another time) to send invalid commands that the servers would blindly forward, and cause the receiving clients to crash. These crashes were more than simply inconvenient; they could also corrupt your save data, destroying the hours of work you may have put into hunting items and leveling up your character.
|
||||
|
||||
For a while it was essentially necessary to use a proxy to go online at all, so the proxy could block these invalid commands. Khyps was designed primarily with this function in mind, though it also implemented some convenient cheats, like the ability to give yourself or other players infinite HP and allow you to teleport to different places without using an in-game teleporter.
|
||||
|
||||
<img align="left" src="static/s-khyller.png" /> After Khyps I took on the larger challenge of writing a server, which resulted in Khyller sometime in 2005. This was the first server of any type I had ever written. This project eventually evolved into a full-featured environment supporting all versions of the game that I had access to - at the time, PC, GC, and BB. (However, I suspect from reading the ancient source files that Khyller's BB support was very buggy.) As Khyller evolved, the code became increasingly cumbersome, littered with debugging filth that I never cleaned up and odd coding patterns I had picked up over the years. My understanding of the C++ language was woefully incomplete as well (as opposed to now, when it is still incomplete but not woefully so), which resulted in Khyller being essentially a C project that had a couple of classes in it.
|
||||
|
||||
<img align="left" src="static/s-aeon.png" /> Sometime in 2006 or 2007, I abandoned Khyller and rebuilt the entire thing from scratch, resulting in Aeon. Aeon was substantially cleaner in code than Khyller but still fairly hard to work with, and it lacked a few of the more arcane features I had originally written (for example, the ability to convert any quest into a download quest). In addition, the code still had some stability problems... it turns out that Aeon's concurrency primitives were simply incorrect. I had derived the concept of a mutex myself, before taking any real computer engineering classes, but had implemented it incorrectly. I made the race window as small as possible, but Aeon would still randomly crash after running seemingly fine for a few days.
|
||||
|
||||
At the time of its inception, Aeon was also called newserv, and you may find some beta releases floating around the Internet with filenames like `newserv-b3.zip`. I had released betas 1, 2, and 3 before I released the entire source of beta 5 and stopped working on the project when I went to college. This was around the time when I switched from writing software primarily on Windows to primarily on macOS and Linux, so Aeon beta 5 was the last server I wrote that specifically targeted Windows. (newserv, which you're looking at now, is difficult to compile on Windows but does work.)
|
||||
|
||||
<img align="left" src="static/s-newserv.png" /> After a long hiatus from PSO and much professional and personal development in my technical abilities, I was reminiscing sometime in October 2018 by reading my old code archives. Somehow inspired when I came across Aeon, I spent a weekend and a couple more evenings rewriting the entire project again, cleaning up ancient patterns I had used eleven years ago, replacing entire modules with simple STL containers, and eliminating even more support files in favor of configuration autodetection. The code is now suitably modern and stable, and I'm not embarrassed by its existence, as I am by Aeon beta 5's source code and my archive of Khyller (which, thankfully, no one else ever saw).
|
||||
|
||||
## Other server projects
|
||||
|
||||
Independently of this project, there are many other PSO servers out there. Those that I know of that are (or were) public are listed here in approximate chronological order:
|
||||
|
||||
* (Early 2000s) **[Schtserv](https://schtserv.com/)**: The first public-access PSO server, written in Delphi by Schthack. Schtserv is the only other unofficial server to support Episode 3, their implementation of which is based on newserv's (which is based on Sega's).
|
||||
* (2005) **Khyller**: An early attempt of mine to support PSO PC, GC, and BB. See above for more details.
|
||||
* (2006) **Aeon**: My second attempt. Better than Khyller, but still unreliable.
|
||||
* (2008) **Tethealla**: A fairly extensive implementation of PSOBB, written in C by Sodaboy. The public version of Tethealla has been [officially disowned](https://www.pioneer2.net/community/threads/tethealla-server-forums-removal.26365/) as it is now more than 15 years old, but closed-source development continues. [Ephinea](https://ephinea.pioneer2.net/) is the continuation of this project. Several other modern PSOBB servers are forks of the initial public version of Tethealla as well.
|
||||
* (2008) **[Sylverant](https://sylverant.net/)** [(source)](https://sourceforge.net/projects/sylverant/): The second public-access PSO server, written in C by BlueCrab.
|
||||
* (2015) **[Archon](https://github.com/dcrodman/archon)**: A PSOBB server written in Go by Drew Rodman.
|
||||
* (2015) **[Idola](https://github.com/HybridEidolon/idolapsoserv)**: A PSOBB server written in Rust by HybridEidolon. Functionality status unknown; the project has been archived.
|
||||
* (2017) **[Aselia](https://github.com/Solybum/Aselia)**: A PSOBB server written in C# by Soly. It seems this was planned to be open-source at some point, but that has not (yet) happened.
|
||||
* (2018) **newserv**: This project right here.
|
||||
* (2019) **[Mechonis](https://gitlab.com/sora3087/mechonis)**: A PSOBB server with a microservice architecture written in TypeScript by TrueVision.
|
||||
* (2020) **[Booma.Server](https://github.com/HelloKitty/Booma.Server)**: A PSOBB server written in C# by Glader, with Soly's help.
|
||||
* (2021) **[Phantasmal World](https://github.com/DaanVandenBosch/phantasmal-world)**: A set of PSO tools, including a web-based model viewer and quest builder, and a PSO server, written by Daan Vanden Bosch.
|
||||
* (2021) **[Elseware](http://git.sharnoth.com/jake/elseware)**: A PSOBB server written in Rust by Jake.
|
||||
|
||||
## Using newserv in other projects
|
||||
|
||||
You are free to use code from newserv in your own open-source projects; the only condition is that the contents of the LICENSE file must be included in your project if you use code from newserv. Your project does not also have to use the MIT license; you can use any license you want.
|
||||
|
||||
If you want to use parts of newserv in your project, there are two easy ways to do so with proper licensing:
|
||||
* If you're using a lot of code from newserv, you can put a copy of newserv's LICENSE file in your repository alongside your own license file, or include the contents of newserv's license in your own license file.
|
||||
* If you're only using a few files from newserv, you can copy and paste the contents of the LICENSE file into a comment at the beginning of each copied file.
|
||||
|
||||
Some of the more likely useful files are:
|
||||
* **src/CommandFormats.hh**: Complete listing of all network commands used in all known versions of the game, and their formats
|
||||
* **src/CommonItemSet.hh/cc**: Format of ItemPT files, shop definition files, and tekker adjustment tables
|
||||
* **src/DCSerialNumbers.hh/cc**: PSO DC serial number validation algorithm and serial number generator
|
||||
* **src/ItemData.hh**: Item format reference
|
||||
* **src/ItemCreator.hh/cc**: Reverse-engineered item generator from Episodes 1&2 (used for all versions)
|
||||
* **src/ItemParameterTable.hh**: Format of many structures in ItemPMT.prs
|
||||
* **src/Map.hh/cc**: Map file (.dat/.evt) structure, listing of object/enemy types and parameters, and reverse-engineered Challenge Mode random enemy generation algorithm
|
||||
* **src/QuestScript.cc**: Complete listing of all quest opcodes on all versions, along with their arguments and behavior
|
||||
* **src/RareItemSet.hh/cc**: Format of ItemRT files (rare item drop tables)
|
||||
* **src/SaveFileFormats.hh**: Definitions of save file structures for all versions
|
||||
* **src/Episode3/DataIndexes.hh**: Episode 3 file structures, including card definition format and map/quest format
|
||||
* **system/item-tables/names-v4.json**: Names of all items, indexed by the first 3 bytes of data1
|
||||
|
||||
## Contributing to newserv
|
||||
|
||||
The goals of this project are:
|
||||
* Build stable, extensible PSO server software that includes all vanilla functionality as well as optional modern conveniences, features, and cheats.
|
||||
* Document the internals of PSO's network protocol, file formats, and game mechanics. This is mainly done through comments in the code.
|
||||
|
||||
This is a personal project; there is no official development team, official website, or official instance of newserv. Issues and pull requests are certainly welcome, but please only add content (e.g. quests or patches) that you've created, is already public, or you have permission to release publicly.
|
||||
|
||||
# Compatibility
|
||||
|
||||
newserv supports all known versions of PSO, including various development prototypes. This table lists all versions that newserv supports. (NTE stands for Network Trial Edition; the GameCube beta versions were called Trial Edition instead, but we use the NTE abbreviation anyway for consistency.)
|
||||
|
||||
| Version | Lobbies | Games | Proxy |
|
||||
|-----------------|----------|----------|----------|
|
||||
| DC NTE | Yes | Yes | Yes |
|
||||
| DC 11/2000 | Yes | Yes | Yes |
|
||||
| DC 12/2000 | Yes | Yes | Yes |
|
||||
| DC 01/2001 | Yes | Yes | Yes |
|
||||
| DC V1 | Yes | Yes | Yes |
|
||||
| DC 08/2001 | Yes | Yes | Yes |
|
||||
| DC V2 | Yes | Yes | Yes |
|
||||
| PC NTE | Yes (1) | Yes | Yes |
|
||||
| PC | Yes | Yes | Yes |
|
||||
| GC Ep1&2 NTE | Yes | Yes | Yes |
|
||||
| GC Ep1&2 | Yes | Yes | Yes |
|
||||
| GC Ep1&2 Plus | Yes | Yes | Yes |
|
||||
| GC Ep3 NTE | Yes | Yes (2) | Yes |
|
||||
| GC Ep3 | Yes | Yes | Yes |
|
||||
| Xbox Ep1&2 Beta | Yes (3) | Yes (3) | Yes (3) |
|
||||
| Xbox Ep1&2 | Yes (3) | Yes (3) | Yes (3) |
|
||||
| BB (vanilla) | Yes | Yes | Yes |
|
||||
| BB (Tethealla) | Yes | Yes | Yes |
|
||||
|
||||
*Notes:*
|
||||
1. *This is the only version of PSO that doesn't have any way to identify the player's account - there is no serial number or username. For this reason, AllowUnregisteredUsers must be enabled in config.json to support PC NTE, and PC NTE players receive a random Guild Card number every time they connect. To prevent abuse, PC NTE support can be disabled in config.json.*
|
||||
2. *Episode 3 NTE battles are not well-tested; some things may not work. See notes/ep3-nte-differences.txt for a list of known differences between NTE and the final version. NTE and non-NTE players cannot battle each other.*
|
||||
3. *PSO Xbox connects through Xbox Live, so you can't easily host a private server for this version of the game. See the [How to connect](#pso-xbox) section.*
|
||||
|
||||
# Setup
|
||||
|
||||
## Server setup
|
||||
|
||||
Currently newserv works on macOS, Windows, and Ubuntu Linux. It will likely work on other Linux flavors too.
|
||||
|
||||
### Windows/macOS
|
||||
|
||||
1. Download the latest release.zip file from the [releases page](https://github.com/fuzziqersoftware/newserv/releases).
|
||||
2. Extract the contents of the archive to some location on your computer.
|
||||
3. Go into the system/ folder, open config.json in a text editor, and edit it to your liking. There are comments in the file that describe what all the options do. Most of the options can be left alone if you want default behavior, but on Windows, you must change LocalAddress and ExternalAddress.
|
||||
4. (Optional) If you plan to play Blue Burst on newserv, set up the patch directory. See [client patch directories](#client-patch-directories) for details.
|
||||
5. Run the newserv executable.
|
||||
|
||||
### Linux
|
||||
|
||||
There are currently no precompiled releases for Linux. To run newserv on Linux, you'll have to build it from source - see the section below.
|
||||
|
||||
### Building from source (macOS/Linux)
|
||||
|
||||
To build on macOS or Linux:
|
||||
|
||||
1. Install the dependencies needed for your platform:
|
||||
* macOS: `brew install cmake asio libiconv`
|
||||
* Linux: `sudo apt-get install cmake libasio-dev` (or use your Linux distribution's package manager)
|
||||
2. Build and install [phosg](https://github.com/fuzziqersoftware/phosg) and [resource_dasm](https://github.com/fuzziqersoftware/resource_dasm).
|
||||
3. Run `cmake . && make` in the newserv directory.
|
||||
|
||||
After building newserv, edit system/config.example.json as needed **and rename it to system/config.json** (note that this step is not necessary for the precompiled releases), set up [client patch directories](#client-patch-directories) if you're planning to play Blue Burst, then run `./newserv` in newserv's directory.
|
||||
|
||||
The server has an interactive shell which can be used to make changes, such as managing user accounts, updating the server's configuration, managing Episode 3 tournaments, and more. Type `help` and press Enter to see all the commands.
|
||||
|
||||
On Linux and macOS, the server also responds to SIGUSR1 and SIGUSR2. SIGUSR1 does the equivalent of the shell's `reload config` command, which reloads config.json but not any dependent files (so quests, Episode 3 maps, etc. will not be reloaded). SIGUSR2 does the equivalent of the shell's `reload all` command, which reloads everything.
|
||||
|
||||
To use newserv in other ways (e.g. for translating data), see the end of this document.
|
||||
|
||||
### Building from source (Windows)
|
||||
|
||||
The current version of newserv is cross-compiled using mingw-w64 on a macOS build machine, with the necessary libraries manually installed. Setting up such a build environment is tedious and not recommended; it's recommended to just use a release version instead.
|
||||
|
||||
Here is a rough outline of the Windows build process. You should only attempt this yourself if you're familiar with setting up build environments and can deal with issues you may encounter along the way.
|
||||
1. Install recent versions of MinGW and CMake.
|
||||
2. Build and install zlib, libiconv, asio, phosg, and resource_dasm into your MinGW environment.
|
||||
3. Clone the newserv repository with symlinks enabled: `git clone -c core.symlinks=true https://github.com/fuzziqersoftware/newserv.git`
|
||||
4. Build newserv via CMake.
|
||||
|
||||
## Client patch directories
|
||||
|
||||
newserv implements a patch server for PSO PC and PSO BB game data. Any file or directory you put in the system/patch-bb or system/patch-pc directories will be synced to clients when they connect to the patch server.
|
||||
|
||||
For Blue Burst set up, the below is mandatory for a smooth experience:
|
||||
|
||||
1. Browse to your chosen client's data directory.
|
||||
2. Copy all the `map_*.dat` files, `map_*.evt`, `unitxt_*` files, and the `data.gsl` file and place them in `system/patch-bb/data`.
|
||||
3. If you're using game files from the Tethealla client, make a copy of `unitxt_j.prs` inside system/patch-bb/data and name it `unitxt_e.prs`. (If `unitxt_e.prs` already exists, replace it with the copied file.)
|
||||
|
||||
If you don't have a BB client, or if you're using a Tethealla client from another source, Tethealla clients that are compatible with newserv can be found here: [English](https://web.archive.org/web/20240402011115/https://ragol.org/files/bb/TethVer12513_English.zip) / [Japanese](https://web.archive.org/web/20240402013127/https://ragol.org/files/bb/TethVer12513_Japanese.zip). These clients connect to 127.0.0.1 (localhost) automatically.
|
||||
|
||||
For BB clients, newserv reads some files out of the patch data to implement game logic, so it's important that certain game files are synchronized between the server and the client. newserv contains defaults for these files in the system/maps/bb-v4 directory, but if these don't match the client's copies of the files, odd behavior will occur in games.
|
||||
|
||||
To make server startup faster, newserv caches the modification times, sizes, and checksums of the files in the patch directories. If the patch server appears to be misbehaving, try deleting the .metadata-cache.json file in the relevant patch directory to force newserv to recompute all the checksums. Also, in the case when checksums are cached, newserv may not actually load the data for a patch file until it's needed by a client. Therefore, modifying any part of the patch tree while newserv is running can cause clients to see an inconsistent view of it.
|
||||
|
||||
Patch directory contents are cached in memory. If you've changed any of these files, you can run `reload patch-indexes` in the interactive shell to make the changes take effect without restarting the server.
|
||||
|
||||
## How to connect
|
||||
|
||||
### PSO DC
|
||||
|
||||
Depending on the version of PSO DC that you have, the instructions to connect to a newserv instance will vary.
|
||||
|
||||
If you have NTE, USv1, EUv1, or EUv2 and a Broadband Adapter, edit the broadband DNS address to newserv's IP address with newserv's DNS server running. Otherwise, it is necessary to patch the disc or use a codebreaker code to remove the Hunter License server check and/or redirect PSO to the newserv instance. Patching the disc or creating a codebreaker code is beyond the scope of this document.
|
||||
|
||||
### PSO DC on Flycast
|
||||
|
||||
If you're emulating PSO DC, the NTE, USv1, EUv1, and EUv2 versions will connect to newserv by setting the following options in Flycast's `emu.cfg` file under `[network]`:
|
||||
- DNS = Your newserv's server address (newserv's DNS server must be running on port 53)
|
||||
- EmulateBBA = yes
|
||||
- Enable = yes
|
||||
|
||||
It is also necessary to save any DNS information to the flash memory of the Dreamcast to use the BBA - the easiest way to do this is to use the website option in USv2 and then choose the save to flash option.
|
||||
|
||||
If the server is running on the same machine as Flycast, this might not work, even if you point Flycast's DNS queries at your local IP address (instead of 127.0.0.1). In this case, you can modify the loaded executable in memory to make it connect anywhere you want. There is a script included with newserv that can do this on macOS; a similar technique could be done manually using scanmem on Linux or Cheat Engine on Windows. To use the script, do this:
|
||||
1. Build and install [memwatch](https://github.com/fuzziqersoftware/memwatch).
|
||||
2. Start Flycast and run PSO. (You must start PSO before running the script; it won't work if you run the script before loading the game.)
|
||||
3. Run `sudo patch_flycast_memory.py <original-destination>`. Replace `<original-destination>` with the hostname that PSO wants to connect to (you can find this out by using Wireshark and looking for DNS queries). The script may take up to a minute; you can continue using Flycast while it runs, but don't start an online game until the script is done.
|
||||
4. Run newserv and start an online game in PSO.
|
||||
|
||||
If you use this method, you'll have to run the script every time you start PSO in Flycast, but you won't have to run it again if you start another online game without restarting emulation.
|
||||
|
||||
If using JPv1, JPv2, or USv2, it is also necessary to remove the Hunter Licence server check, either with a disc patch or codebreaker code. Patching the disc or creating a codebreaker code is beyond the scope of this document.
|
||||
|
||||
### PSO PC
|
||||
|
||||
PSO PC has its connection addresses in `pso.exe`. Hex edit the executable with the connection address you want to connect to. Common server addresses to search for to replace are:
|
||||
- pso20.sonic.isao.net
|
||||
- sg207634.sonicteam.com
|
||||
- pso-mp01.sonic.isao.net
|
||||
- gsproduc.ath.cx
|
||||
- sylverant.net
|
||||
|
||||
The version of PSO PC I have has the server addresses starting at offset 0x29CB34 in pso.exe. Change those addresses to "localhost" (without quotes) if you just want to connect to a locally-running newserv instance. Alternatively, you can add an entry to the Windows hosts file (C:\Windows\System32\drivers\etc\hosts) to redirect the connection to 127.0.0.1 (localhost) or any other IP address.
|
||||
|
||||
### PSO GC on a real GameCube
|
||||
|
||||
You can make PSO connect to newserv by setting the default gateway and DNS server addresses in the game's network settings to newserv's address. newserv's DNS server must be running on port 53 and must be accessible to the GameCube. If you're not playing PSO Plus or Episode III, this should be all you need to do, assuming you already set LocalAddress in config.json to your PC's private IP address.
|
||||
|
||||
If you have PSO Plus or Episode III, it won't want to connect to a server on the same local network as the GameCube itself, as determined by the GameCube's IP address and subnet mask. There are a couple of ways to get around this.
|
||||
|
||||
Sodaboy described a fairly easy method, which is to forward the PSO and DNS ports in your router's configuration to your PC's private IP address (the PSO ports are in config.json, and are all TCP; the DNS port is 53 and is UDP). Then, set LocalAddress and ExternalAddress in config.json to your external IP address (from e.g. whatismyip.com). Most routers will let you connect to your public IP address even from within the local network, but the GameCube will think it's connecting to a different network, so it won't reject the connection. If you're concerned about security and don't want your server to be publicly accessible, you can use Windows Firewall or UFW on Linux block incoming connections on the ports you opened, except for connections from the IP addresses you specify.
|
||||
|
||||
Another method is to use two network interfaces on the same PC, and tell the GameCube to connect to the one that appears to be on a different network. For example, if your GameCube is on the 10.0.0.x subnet and your PC's address is 10.0.0.5, you can create a fake network adapter on your PC (or use an existing real one) that has an IP address on a different subnet than the GameCube, such as 192.168.0.8. Then, in PSO's network config, set the default gateway and DNS server addresses to 192.168.0.8, and set LocalAddress in config.json to 192.168.0.8, and PSO should connect. This is what I did back in the old days when I primarily developed software on Windows, but I haven't tried it in many years.
|
||||
|
||||
### PSO GC on a Wii or Wii U
|
||||
|
||||
Using a Wii or Wii U to connect to newserv requires the Wii or vWii to be softmodded. How to do this is beyond the scope of this document.
|
||||
|
||||
Nintendont includes BBA emulation and is compatible with all PSO GameCube versions except Episodes I&II Trial Edition. To use Nintendont, enable BBA emulation in Nintendont's settings and follow the instructions in the above section (PSO GC on a real GameCube).
|
||||
|
||||
Devolution includes modem emulation and is compatible with all PSO GameCube versions including Episodes I&II Trial Edition. newserv can act as a PPP server, which Devolution can directly connect to. To do this:
|
||||
1. Enable the PPPRawListen option according to the comments in config.json.
|
||||
2. Start newserv.
|
||||
3. In the game's network settings, set the username and password to anything (they cannot be blank), and set the phone number to the number that newserv outputs to the console during startup. (It will be near the end of all the startup log messages.) If your Wii is on the same network as newserv, use the local number; otherwise, use the external number.
|
||||
|
||||
### PSO GC on Dolphin
|
||||
|
||||
If you're using the HLE BBA type, set the BBA's DNS server address to newserv's IP address and it should work. (If newserv is on the same machine as Dolphin, you will need to use an Action Replay code directed at 127.0.0.1 to connect, as PSO rejects DNS queries from the same IP address.) Set PSO's network settings the same as listed below.
|
||||
|
||||
If you're using the TAP (not tapserver) BBA type, you'll have to set PSO's network settings appropriately for your tap interface. Set the DNS server address in PSO's network settings to newserv's IP address.
|
||||
|
||||
If you're using the tapserver BBA or modem type, you can make it connect to a newserv instance running on the same machine via the tapserver interface. To do this:
|
||||
1. In the GameCube pane of the Config window, set the SP1 device to Broadband Adapter (tapserver) or Modem Adapter (tapserver).
|
||||
2. Click the "..." button next to the SP1 menu. If you're using the tapserver BBA, enter `127.0.0.1:5059` in the box. If you're using the tapserver modem, enter `127.0.0.1:5058` in the box. (If newserv isn't running on the same machine as Dolphin, replace 127.0.0.1 with newserv's IP address.)
|
||||
3. In PSO's network settings, enable DHCP ("Automatically obtain an IP address"), set DNS server address to "Automatic", and leave DHCP Hostname as "Not set". Leave the proxy server settings blank.
|
||||
4. Start an online game.
|
||||
|
||||
### PSO Xbox
|
||||
|
||||
Unfortunately, you can't easily host a private server for PSO Xbox because the Xbox version of the game tunnels its connections through Xbox Live. There is a modern replacement for Xbox Live named [Insignia](https://insignia.live/), which supports the three main PSO Xbox servers, but as of now does not support other private PSO servers.
|
||||
|
||||
### PSO BB
|
||||
|
||||
The PSO BB client has been modified and distributed in many different forms. newserv supports most, but not all, of the common distributions. Unlike other versions, it's common for various BB clients to have different map files. It's important that the client and server have the same map files, so make sure to set up the patch directory based on the client you'll be using with newserv. (See the [client patch directories](#client-patch-directories) section for instructions on setting this up.)
|
||||
|
||||
The original Japanese and US versions of PSO BB work with newserv (the last Japanese release can be found [here](https://archive.org/details/psobb_jp_setup_12511_20240109/)). To get them to connect to your server, do one of the following:
|
||||
* Use a drop-in patcher like [AzureFlare](https://github.com/Repflez/AzureFlare).
|
||||
* Edit your hosts file to redirect the client's destination address to localhost or your server's address.
|
||||
* Edit psobb.exe to point to your newserv instance. The original clients are packed with various versions of ASProtect, so this is a more involved process than simply opening the executable in a hex editor and finding/replacing some strings.
|
||||
|
||||
Alternatively, you can use the Tethealla client ([English](https://web.archive.org/web/20240402011115/https://ragol.org/files/bb/TethVer12513_English.zip) or [Japanese](https://web.archive.org/web/20240402013127/https://ragol.org/files/bb/TethVer12513_Japanese.zip)). If the server is on the same PC as the client and you don't plan to have any external players, these Tethealla clients will automatically connect to the server without any modifications. This version of the client is not packed, and you can find the connection addresses starting at offset 0x56D724 in psobb.exe. Overwrite these addresses with your server's hostname or IP address, and you should be able to connect.
|
||||
|
||||
### Allowing external players to connect
|
||||
|
||||
If you want to accept connections from outside your local network, you'll need to set ExternalAddress to your public IP address in the configuration file, and you'll likely need to open some ports in your router's NAT configuration - specifically, all the TCP ports listed in PortConfiguration in config.json.
|
||||
|
||||
For GC clients, you'll have to use newserv's built-in DNS server or set up your own DNS server as well. If you want external clients to be able to use your DNS server, you'll have to forward UDP port 53 to your newserv instance. Remote players can then connect to your server by entering your DNS server's IP address in their client's network configuration.
|
||||
|
||||
# Server feature configuration
|
||||
|
||||
## User accounts
|
||||
|
||||
By default, newserv does not require users to pre-register before playing; the server will instead automatically create an account the first time each player connects. These accounts have no special permissions. You can view, create, edit, and delete user accounts in the server's shell (run `help` in the shell to see how to do this).
|
||||
|
||||
A license is a set of credentials that a player can use to log in. There are six types of licenses:
|
||||
* *DC NTE licenses* consist of a 16-character serial number and 16-character access key.
|
||||
* *DC licenses* consist of an 8-character hex serial number and an 8-character access key.
|
||||
* *PC licenses* are the same format as DC licenses, but are used for PC v2.
|
||||
* *GC licenses* consist of a 10-digit decimal serial number, a 12-character access key, and a password of up to 8 characters.
|
||||
* *XB licenses* consist of a gamertag of up to 16 characters, a 16-character hex user ID, and a 16-character hex account ID.
|
||||
* *BB licenses* consist of a username of up to 16 characters and a password of up to 16 characters.
|
||||
|
||||
Each account may have multiple licenses. To add a license to an existing account, use `add-license` in the shell.
|
||||
|
||||
On BB, character data is scoped to the license, but system and Guild Card data is scoped to the account. That is, an account with multiple BB licenses can have more than 4 characters (up to 4 per license), but they will all share the same team membership and Guild Card lists.
|
||||
|
||||
You may want to give your account elevated privileges. To do so, run `update-account ACCOUNT-ID flags=root` (replacing ACCOUNT-ID with your actual account-id). You can also use update-account to edit other parts of the account; see the help text for more information.
|
||||
|
||||
## Installing quests
|
||||
|
||||
newserv automatically finds quests in the subdirectories of the system/quests/ directory. To install your own quests, or to use quests you've saved using the proxy's Save Files option, just put them in one of the subdirectories there and name them appropriately. The subdirectories and their behaviors (e.g. in which game modes they should appear and for which PSO versions) is defined in the QuestCategories field in config.json.
|
||||
|
||||
Within the category directories, quest files should be named like `q###-VERSION-LANGUAGE.EXT` (although the `q` is ignored, and can be any letter). The fields in each filename are:
|
||||
- `###`: quest number (this doesn't really matter; it should just be unique across the PSO version)
|
||||
- `VERSION`: dn = Dreamcast NTE, dp = Dreamcast 11/2000 prototype, d1 = Dreamcast v1, dc = Dreamcast v2, pcn = PC NTE, pc = PC, gcn = GameCube NTE, gc = GameCube Episodes 1 & 2, gc3 = Episode 3 (see below), xb = Xbox, bb = Blue Burst
|
||||
- `LANGUAGE`: j = Japanese, e = English, g = German, f = French, s = Spanish
|
||||
- `EXT`: file extension (see table below)
|
||||
|
||||
For .dat files, the `LANGUAGE` token may be omitted. If it's present, then that .dat file will only be used for that language of the quest; if omitted, then that .dat file will be used for all languages of the quest.
|
||||
|
||||
For example, the GameCube version of Lost HEAT SWORD is in two files named `q058-gc-e.bin` and `q058-gc.dat`. newserv knows these files are quests because they're in the system/quests/ directory, it knows they're for PSO GC because the filenames contain `-gc`, it knows this is the English version of the quest because the .bin filename ends with `-e` (even though the .dat filename does not), and it puts them in the Retrieval category because the files are within the retrieval/ directory within system/quests/.
|
||||
|
||||
Some quests have additional JSON metadata files that describe how the server should handle them. These metadata files are generally named similarly to their .bin and .dat counterparts, except the `VERSION` token may also be omitted if the metadata applies to all languages of the quest on all PSO versions. See the comments in system/quests/retrieval/q058.json for all of the available options and how to use them. Some of the options are:
|
||||
- Disable or hide the quest if certain preceding quests aren't cleared or other conditions aren't met
|
||||
- Enable the quest to be joined while in progress
|
||||
- Override the common and/or rare item tables and set the allowed drop modes
|
||||
|
||||
Some quests may also include a .pvr file, which contains an image used in the quest. These files are named similarly to their .bin and .dat counterparts.
|
||||
|
||||
The GameCube and Xbox quest formats are very similar, but newserv treats them as different. If you want to use the same quest file for GameCube and Xbox clients, you can make one a symbolic link to the other.
|
||||
|
||||
There are multiple PSO quest formats out there; newserv supports all of them. It can also decode any known format to standard .bin/.dat format. Specifically:
|
||||
|
||||
| Format | Extension | Supported | Decode action |
|
||||
|------------------|-----------------------|------------|------------------|
|
||||
| Compressed | .bin and .dat | Yes | None (1) |
|
||||
| Compressed Ep3 | .bin or .mnm | Yes (4) | None (1) |
|
||||
| Uncompressed | .bind and .datd | Yes | compress-prs (2) |
|
||||
| Uncompressed Ep3 | .bind or .mnmd | Yes (4) | compress-prs (2) |
|
||||
| Source | .bin.txt and .dat | Yes | None (5) |
|
||||
| VMS (DCv1) | .bin.vms and .dat.vms | Yes | decode-vms |
|
||||
| VMS (DCv2) | .bin.vms and .dat.vms | Decode (3) | decode-vms (3) |
|
||||
| GCI (decrypted) | .bin.gci and .dat.gci | Yes | decode-gci |
|
||||
| GCI (with key) | .bin.gci and .dat.gci | Yes | decode-gci |
|
||||
| GCI (no key) | .bin.gci and .dat.gci | Decode (3) | decode-gci (3) |
|
||||
| GCI (Ep3 NTE) | .bin.gci or .mnm.gci | Decode (3) | decode-gci (3) |
|
||||
| GCI (Ep3) | .bin.gci or .mnm.gci | Yes | decode-gci |
|
||||
| DLQ | .bin.dlq and .dat.dlq | Yes | decode-dlq |
|
||||
| DLQ (Ep3) | .bin.dlq or .mnm.dlq | Yes | decode-dlq |
|
||||
| QST (online) | .qst | Yes | decode-qst |
|
||||
| QST (download) | .qst | Yes | decode-qst |
|
||||
|
||||
*Notes:*
|
||||
1. *This is the default format. You can convert these to uncompressed format by running `newserv decompress-prs FILENAME.bin FILENAME.bind` (and similarly for .dat -> .datd)*
|
||||
2. *Similar to (1), to compress an uncompressed quest file: `newserv compress-prs FILENAME.bind FILENAME.bin` (and likewise for .datd -> .dat)*
|
||||
3. *Use the decode action to convert these quests to .bin/.dat format before putting them into the server's quests directory. If you know the encryption seed (serial number), pass it in as a hex string with the `--seed=` option. If you don't know the encryption seed, newserv will find it for you, which will likely take a long time.*
|
||||
4. *Episode 3 quests don't go in the system/quests directory. See the [Episode 3 section](#episode-3-features) section below.*
|
||||
5. *Quest source can be assembled into a .bin or .bind file with `newserv assemble-quest-script FILENAME.txt`. See system/quests/retrieval/q058-gc-e.bin.txt for an annotated example; this is the English GameCube version of Lost HEAT SWORD.*
|
||||
|
||||
Episode 3 download quests consist only of a .bin file - there is no corresponding .dat file. Episode 3 download quest files may be named with the .mnm extension instead of .bin, since the format is the same as the standard map files (in system/ep3/maps/). These files can be encoded in any of the formats described above, except .qst.
|
||||
|
||||
When newserv indexes the quests during startup, it will warn (but not fail) if any quests are corrupt or in unrecognized formats.
|
||||
|
||||
Quest contents are cached in memory, but if you've changed the contents of the quests directory, you can re-index the quests without restarting the server by running `reload quest-index` in the interactive shell. The new quests will be available immediately, but any games with quests already in progress will continue using the old versions of the quests until those quests end.
|
||||
|
||||
## Item tables and drop modes
|
||||
|
||||
newserv supports server-side item generation on all game versions, except for the earliest DC prototypes (NTE and 11/2000). By default, the game behaves as it did on the original servers - on all versions except BB, item drops are controlled by the leader client in each game, and on BB, item drops are controlled by the server.
|
||||
|
||||
There are five different available behaviors for item drops:
|
||||
* `disabled` (or `none`): No items will drop from boxes or enemies.
|
||||
* `client`: The game leader generates items, all items are visible to all players, and any player may pick up any item. This is the default mode for all game versions, except this mode cannot be used if the game leader is on BB.
|
||||
* `shared`: The server generates items, all items are visible to all players, and any player may pick up any item. This is the default mode if the game leader is on BB.
|
||||
* `private`: The server generates items, but each player may get a different item from any box or enemy. If a player isn't in the same area as an enemy at the time it's defeated, they won't get any item from it. Items dropped by players are visible to everyone.
|
||||
* `duplicate`: The server generates items, and each player will get the same item from any box or enemy, but there is one copy of each item for each player (and each player only sees their own copy of the item). If a player isn't in the same area as an enemy at the time it's defeated, they won't get any item from it. Items dropped by players are not duplicated and are visible to everyone.
|
||||
|
||||
In the `private` and `duplicate` modes, there is no incentive to pick up items before another player, since other players cannot pick up the items you see dropped from boxes and enemies. However, if you pick up an item and drop it later, it can then be seen and picked up by any player.
|
||||
|
||||
The drop mode can be changed at any time during a game with the `$dropmode` chat command. If the mode is changed after some items have already been dropped, the existing items retain their visibility (that is, items dropped in private mode still can't be picked up by other players since they were dropped before the mode was changed). You can configure which drop modes are used by default, and which modes players are allowed to choose, in config.json. See the comments above the AllowedDropModes and DefaultDropMode keys.
|
||||
|
||||
In the server drop modes, the item tables used to generate common items are in the `system/item-tables/ItemPT-*` files. (The V2 files are used for V1 as well.) The rare item tables are in the `rare-table-*.json` files. Unlike the original formats, it's possible to make each enemy drop multiple different rare items at different rates, though the default tables never do this.
|
||||
|
||||
## Cross-version play
|
||||
|
||||
All versions of PSO can see and interact with each other in the lobby. By default, newserv allows V1 and V2 players to play in games together, and allows GC and Xbox players to play in games together. You can change these rules to allow all versions to play in games together, or to prevent versions from playing in games together, with the CompatibilityGroups setting in config.json.
|
||||
|
||||
There are several cross-version restrictions that always apply regardless of the compatibility groups setting:
|
||||
* DC V1 players cannot join DC V2 games if the game creator didn't choose to allow them.
|
||||
* DC V1 players cannot join games if the difficulty level is set to Ultimate or the game mode is Battle or Challenge.
|
||||
* Only GC, Xbox, and BB players can join games in Episode 2.
|
||||
* Only BB players can join games in Episode 4.
|
||||
* Episode 3 players cannot join non-Episode 3 games, and vice versa.
|
||||
|
||||
V1/V2 compatibility and GC/Xbox compatibility are well-tested, but other situations are not. Not much attention has been given yet to how items should be handled across major versions; if you enable V2/GC compatibility, for example, there will likely be bugs. Please report such bugs as GitHub issues.
|
||||
|
||||
In cross-version play, when any of the server drop modes are used, the server uses the drop tables corresponding to the leader's version and section ID. (For example, if a DC V1 player is the game leader, rare-table-v1.json will be used, even after V2 players join.) If a BB player is the leader and the `client` drop mode is used, the server generates items as if it were in `shared` mode.
|
||||
|
||||
## Server-side saves
|
||||
|
||||
newserv has the ability to save character data on the server side. For PSO BB, this is required of course, but this feature can also be used on other PSO versions.
|
||||
|
||||
Each account has 4 BB character slots and 16 non-BB character file slots. The non-BB slots are independent of the BB slots, and can be accessed with the `$savechar <slot>` and `$loadchar <slot>` commands (slots are numbered 1 through 16). `$savechar` copies the character you're currently playing as and saves the data on the server, and `$loadchar` does the reverse, overwriting your current character with the data saved on the server. Note that you can load a character that was saved from a different version of PSO, which allows you to easily transfer characters between games. On v1 and v2, changes done by `$loadchar` will be undone if you join a game; to permanently save your changes, disconnect from the lobby after using the command.
|
||||
|
||||
You can see basic information about a character saved on the server (without affecting your current character) by using `$checkchar <slot>`. You can delete a previously-saved character with `$deletechar <slot>`.
|
||||
|
||||
There is also the command `$bbchar <username> <password> <slot>`, which behaves similarly to `$savechar` but writes the character data to a BB character slot in a different account instead (slots are numbered 1 through 4). This can be used to "upgrade" a character to BB from an earlier version.
|
||||
|
||||
Exactly which data is saved and loaded depends on the game version:
|
||||
|
||||
| Game | Inventory | Character | Options/chats | Quest flags | Bank | Battle/Challenge |
|
||||
|----------------------|-----------|-----------|---------------|-------------|------|------------------|
|
||||
| PSO DC v1 prototypes | Yes | Yes | No | No | No | N/A |
|
||||
| PSO DC v1 | Yes | Yes | No | No | No | N/A |
|
||||
| PSO DC v2 | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| PSO PC (v2) | Yes | Yes | No | No | No | Save only |
|
||||
| PSO GC NTE | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| PSO GC (not Plus) | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| PSO GC Plus (1) | Save only | Save only | No | No | No | Save only |
|
||||
| PSO GC Ep3 (1) | No | Save only | No | No | No | Save only |
|
||||
| PSO Xbox | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| PSO BB | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
|
||||
*Notes*:
|
||||
1. *If EnableSendFunctionCallQuestNumber is enabled in config.json, then $savechar and $loadchar can save and restore all character data on these versions, just like on GC non-Plus. Episode 3 characters exist in a separate namespace; that is, you can't use $savechar and $loadchar to convert an Ep3 character to non-Ep3, or vice versa.*
|
||||
|
||||
## Episode 3 features
|
||||
|
||||
newserv supports many features unique to Episode 3:
|
||||
* CARD battles. Not every combination of abilities has been tested yet, so if you find a feature or card ability that doesn't work like it's supposed to, please make a GitHub issue and describe the situation (the attacking card(s), defending card(s), and ability or condition that didn't work).
|
||||
* Spectator teams.
|
||||
* Tournaments. (But they work differently than Sega's tournaments did - see below)
|
||||
* Downloading quests.
|
||||
* Trading cards.
|
||||
* Participating in card auctions. (The auction contents must be configured in config.json.)
|
||||
* Decorations in lobbies. Currently only images are supported; the game also supports loading custom 3D models in lobbies, but newserv does not implement this (yet).
|
||||
|
||||
### Battle records
|
||||
|
||||
After playing a battle, you can save the record of the battle with the `$saverec` command. You can then replay the battle later by using the `$playrec` command in a lobby - this will create a spectator team and play the recording of the battle as if it were happening in realtime. Note that there is a bug in older versions of Dolphin that seems to be frequently triggered when playing battle records, which causes the emulator to crash with the message `QObject::~QObject: Timers cannot be stopped from another thread`. To avoid this, use the latest version of Dolphin.
|
||||
|
||||
### Tournaments
|
||||
|
||||
Tournaments work differently than they did on Sega's servers. Tournaments can be created with the `create-tournament` shell command, which enables players to register for them. (Use `help` to see all the arguments - there are many!) The `start-tournament` shell command starts the tournament (and prevents further registrations), but this doesn't schedule any matches. Instead, players who are ready to play their next match can all stand at the 4-player battle table near the lobby warp in the same CARD lobby, and the tournament match will start automatically.
|
||||
|
||||
These tournament semantics mean that there can be multiple matches in the same tournament in play simultaneously, and not all matches in a round must be complete before the next round can begin - only the matches preceding each individual match must be complete for that match to be playable.
|
||||
|
||||
The Meseta rewards for winning tournament matches can be configured in config.json.
|
||||
|
||||
### Episode 3 files
|
||||
|
||||
Episode 3 state and game data is stored in the system/ep3 directory. The files in there are:
|
||||
* card-definitions.mnr: Compressed card definition list, sent to Episode 3 clients at connect time. Card stats and abilities can be changed by editing this file.
|
||||
* card-definitions.mnrd: Decompressed version of the above. If present, newserv will use this instead of the compressed version, since this is easier to edit.
|
||||
* card-text.mnr: Compressed card text archive. Generally only used for debugging.
|
||||
* card-text.mnrd: Decompressed card text archive; same format as TextCardE.bin. Generally only used for debugging.
|
||||
* com-decks.json: COM decks used in tournaments. The default decks in this file come from logs from Sega's servers, so the file doesn't include every COM deck Sega ever made - the rest are probably lost to time.
|
||||
* maps/: Online free battle and quest maps (.mnm/.bin/.mnmd/.bind files). newserv comes with the default online maps, as well as some fan-made variations and quests to help new players get up to speed. Within the maps/ directory, each subdirectory is treated as a separate category and may be optionally downloadable or available at the battle setup counter. The category.json file in each subdirectory specifies the category's behavior; see system/ep3/maps/online/category.json for a documented example.
|
||||
* tournament-state.json: State of all active tournaments. This file is automatically written when any tournament changes state for any reason (e.g. a tournament is created/started/deleted or a match is resolved).
|
||||
|
||||
There is no public editor for Episode 3 maps and quests, but the format is described fairly thoroughly in src/Episode3/DataIndexes.hh (see the MapDefinition structure). You'll need to use `newserv decompress-prs ...` to decompress a .bin or .mnm file before editing it, but you don't need to compress it again to use it - just put the .bind or .mnmd file in the maps directory and newserv will make it available.
|
||||
|
||||
Like quests, Episode 3 card definitions, maps, and quests are cached in memory. If you've changed any of these files, you can run `reload ep3-cards` or `reload ep3-maps` in the interactive shell to make the changes take effect without restarting the server.
|
||||
|
||||
## Memory patches and client functions
|
||||
|
||||
You can put assembly files in the system/client-functions directory with filenames like PatchName.VERS.patch.s and they will appear in the Patches menu for clients that support client functions. Client functions are written in SH-4, PowerPC, or x86 assembly and are compiled when newserv is started. The assembly system's features are documented in the comments in system/client-functions/System/WriteMemoryGC.ppc.s.
|
||||
|
||||
The VERS token in client function filenames refers to the specific version of the game that the client function applies to. Some versions do not support receiving client functions at all. *Note: newserv uses the shorter GameCube versioning convention, where discs labeled DOL-XXXX-0-0Y are version 1.Y. The PSO community seems to use the convention 1.0Y in some places instead, but these are the same version. For example, the version that newserv calls v1.4 is the same as v1.04, and is labeled DOL-GPOJ-0-04 on the underside of the disc.*
|
||||
|
||||
The specific versions are:
|
||||
|
||||
| Game | VERS | CPU architecture |
|
||||
|------------------------------|------|--------------------------------|
|
||||
| PSO DC Network Trial Edition | 1OJ1 | Client functions not supported |
|
||||
| PSO DC 11/2000 prototype | 1OJ2 | Client functions not supported |
|
||||
| PSO DC 12/2000 prototype | 1OJ3 | Client functions not supported |
|
||||
| PSO DC 01/2001 prototype | 1OJ4 | Client functions not supported |
|
||||
| PSO DC v1 JP | 1OJF | Client functions not supported |
|
||||
| PSO DC v1 US | 1OEF | Client functions not supported |
|
||||
| PSO DC v1 EU | 1OPF | Client functions not supported |
|
||||
| PSO DC 08/06/2001 prototype | 2OJ4 | SH-4 |
|
||||
| PSO DC 08/22/2001 prototype | 2OJ5 | SH-4 |
|
||||
| PSO DC v2 JP | 2OJF | SH-4 |
|
||||
| PSO DC v2 US | 2OEF | SH-4 |
|
||||
| PSO DC v2 EU | 2OPF | SH-4 |
|
||||
| PSO PC (v2) Trial Edition | 2OJT | Client functions not supported |
|
||||
| PSO PC (v2) 04/2002 | 2OJW | Client functions not supported |
|
||||
| PSO PC (v2) 02/2003 | 2OJZ | Client functions not supported |
|
||||
| PSO GC Trial Edition | 3OJT | PowerPC |
|
||||
| PSO GC v1.2 JP | 3OJ2 | PowerPC |
|
||||
| PSO GC v1.3 JP | 3OJ3 | PowerPC |
|
||||
| PSO GC v1.4 (Plus) JP | 3OJ4 | PowerPC |
|
||||
| PSO GC v1.5 (Plus) JP | 3OJ5 | PowerPC (1) |
|
||||
| PSO GC v1.0 US | 3OE0 | PowerPC |
|
||||
| PSO GC v1.1 US | 3OE1 | PowerPC |
|
||||
| PSO GC v1.2 (Plus) US | 3OE2 | PowerPC (1) |
|
||||
| PSO GC v1.0 EU | 3OP0 | PowerPC |
|
||||
| PSO GC Ep3 Trial Edition | 3SJT | PowerPC |
|
||||
| PSO GC Ep3 JP | 3SJ0 | PowerPC |
|
||||
| PSO GC Ep3 US | 3SE0 | PowerPC (1) |
|
||||
| PSO GC Ep3 EU | 3SP0 | PowerPC (1) |
|
||||
| PSO Xbox Beta | 4OJB | x86 |
|
||||
| PSO Xbox JP Disc | 4OJD | x86 |
|
||||
| PSO Xbox JP TU | 4OJU | x86 |
|
||||
| PSO Xbox US Disc | 4OED | x86 |
|
||||
| PSO Xbox US TU | 4OEU | x86 |
|
||||
| PSO Xbox EU Disc | 4OPD | x86 |
|
||||
| PSO Xbox EU TU | 4OPU | x86 |
|
||||
| PSO BB JP 1.25.11 | 59NJ | x86 |
|
||||
| PSO BB JP 1.25.13 | 59NL | x86 |
|
||||
| PSO BB Tethealla | 59NL | x86 |
|
||||
|
||||
*Notes:*
|
||||
1. *Client functions are only supported on these versions if EnableSendFunctionCallQuestNumbers is set in config.json. See the comments there for more information.*
|
||||
|
||||
newserv comes with a set of patches for many of the above versions. These are organized in subdirectories within system/client-functions/.
|
||||
|
||||
### DOL loader
|
||||
|
||||
You can put DOL files in the system/dol directory, and they will appear in the Programs menu for GC clients. Selecting a DOL file there will load the file into the GameCube's memory and run it, just like the old homebrew loaders (PSUL and PSOload) did. For this to work, ReadMemoryWordGC.ppc.s, WriteMemoryGC.ppc.s, and RunDOL.ppc.s must be present in the system/client-functions/System directory. This has been tested on Dolphin but not on a real GameCube, so results may vary.
|
||||
|
||||
Like other kinds of data, functions and DOL files are cached in memory. If you've changed any of these files, you can run `reload functions` or `reload dol-files` in the interactive shell to make the changes take effect without restarting the server.
|
||||
|
||||
I mainly built the DOL loading functionality for documentation purposes. By now, there are many better ways to load homebrew code on an unmodified GameCube, but to my knowledge there isn't another open-source implementation of this method in existence.
|
||||
|
||||
## Using newserv as a proxy
|
||||
|
||||
If you want to play online on remote servers rather than running your own server, newserv also includes a PSO proxy. Currently this works with PSO GC and may work with PC and DC; it also works with some BB clients in specific situations.
|
||||
|
||||
To use the proxy for PSO DC, PC, or GC, add an entry to the corresponding ProxyDestinations dictionary in config.json, then run newserv and connect to it as normal (see below). You'll see a "Proxy server" option in the main menu, and you can pick which remote server to connect to.
|
||||
|
||||
To use the proxy for PSO BB, set the ProxyDestination-BB entry in config.json. If this option is set, it essentially disables the game server for all BB clients - all BB clients will be proxied to the specified destination instead. Unfortunately, because PSO BB uses a different set of handlers for the data server phase and character selection, there's no in-game way to present the player with a list of options, like there is on PSO PC and PSO GC.
|
||||
|
||||
When you're on PSO DC, PC, GC, or Xbox and are connected to a remote server through newserv's proxy, choosing the Change Ship or Change Block action from the lobby counter will send you back to newserv's main menu instead of the remote server's ship or block select menu. You can go back to the server you were just on by choosing it from the proxy server menu again.
|
||||
|
||||
There are many options available when starting a proxy session. All options are off by default unless otherwise noted. The options are:
|
||||
* **Chat commands**: enables chat commands in the proxy session (on by default).
|
||||
* **Chat filter**: enables escape sequences in chat messages and info board (on by default).
|
||||
* **Player notifications**: shows a message when any player joins or leaves the game or lobby you're in.
|
||||
* **Block pings**: blocks automatic pings sent by the client, and responds to ping commands from the server automatically.
|
||||
* **Infinite HP**: automatically heals you whenever you get hit. An attack that kills you in one hit will still kill you, however.
|
||||
* **Infinite TP**: automatically restores your TP whenever you use any technique.
|
||||
* **Switch assist**: unlocks doors that require two or four players in a one-player game, when you step on one of the switches.
|
||||
* **Infinite Meseta** (Episode 3 only): gives you 1,000,000 Meseta, regardless of the value sent by the remote server.
|
||||
* **Block events**: disables holiday events sent by the remote server.
|
||||
* **Block patches**: prevents any B2 (patch) commands from reaching the client.
|
||||
* **Save files**: saves copies of several kinds of files when they're sent by the remote server. The files are written to the current directory (which is usually the directory containing the system/ directory). Saved files can then be used with newserv by just moving the file into the appropriate place in the system/ directory and renaming it appropriately. These kinds of files can be saved:
|
||||
* Online quests and download quests (saved as .bin/.dat files)
|
||||
* GBA games (saved as .gba files)
|
||||
* Patches (saved as .bin files and disassembled as .txt files)
|
||||
* Player, system, and Guild Card data from BB sessions (saved as .psochar, .psosys, .psosysteam, and .psocard files)
|
||||
* Stream file data from BB sessions (saved as ItemPMT, BattleParamEntry, ItemMagEdit, and PlyLevelTbl files)
|
||||
* Episode 3 online quests and maps (saved as .mnmd files)
|
||||
* Episode 3 download quests (saved as .mnm files)
|
||||
* Episode 3 card definitions (saved as .mnr files)
|
||||
* Episode 3 media updates (saved as .gvm, .bml, or .bin files)
|
||||
|
||||
The remote server will probably try to assign you a Guild Card number that doesn't match the one you have on newserv. The proxy rewrites the commands in transit to make it look like the remote server assigned you the same Guild Card number as you have on newserv, but if the remote server has some external integrations (e.g. forum or Discord bots), they will use the Guild Card number that the remote server believes it has assigned to you. The number assigned by the remote server is shown to you when you first connect to the remote server, and you can retrieve it in lobbies or during games with the `$li` command.
|
||||
|
||||
Some chat commands (see below) have the same basic function on the proxy but have different effects or conditions. In addition, there are some server shell commands that affect clients on the proxy (run `help` in the shell to see what they are). If there's only one proxy session open, the shell's proxy commands will affect that session. Otherwise, you'll have to specify which session to affect with the `on` prefix - to send a chat message in C-17's session, for example, you would run `on C-17 chat ...`.
|
||||
|
||||
## Chat commands
|
||||
|
||||
newserv supports a variety of commands players can use by chatting in-game. Any chat message that begins with `$` is treated as a chat command. (If you actually want to send a chat message starting with `$`, type `$$` instead.) On the DC 11/2000 prototype, `@` is used instead of `$` for all chat commands, since `$` does not appear on the English virtual keyboard.
|
||||
|
||||
Some commands only work for clients not in proxy sessions. The chat commands are:
|
||||
|
||||
* Information commands
|
||||
* `$li`: Show basic information about the lobby or game you're in. If you're on the proxy, show information about your connection instead (remote Guild Card number, client ID, etc.).
|
||||
* `$si`: Show basic information about the server.
|
||||
* `$ping`: Show round-trip ping time from the server to you. On the proxy, show the ping time from you to the proxy and from the proxy to the server.
|
||||
* `$matcount` (non-proxy only): Show how many of each type of material you've used.
|
||||
* `$killcount` (non-proxy only): Show the kill count on your currently-equipped weapon. If you're in a game and not on BB, the value is only accurate at the time the item enters the game.
|
||||
* `$itemnotifs <mode>`: Enable item drop notification messages. If the game has private drops enabled, you will only see a notification if the dropped item is visible to you; you won't be notified of other players' drops. The modes are:
|
||||
* `off`: No notifications are shown.
|
||||
* `rare`: You are notified when a rare item drops.
|
||||
* `on`: You are notified when any item drops, except Meseta.
|
||||
* `every`: You are notified when any item drops, including Meseta.
|
||||
* `$announcerares`: Enable or disable announcements for your rare item finds. This determines whether rare items you find will be announced to the game and server, not whether you will see announcements for others finding rare items.
|
||||
* `$what` (non-proxy only): Show the type, name, and stats of the nearest item on the ground.
|
||||
* `$where`: Show your current floor number and coordinates. Mainly useful for debugging.
|
||||
* `$qfread <field-name>` (non-proxy only): Show the value of a quest counter in your player data. The field names are defined in config.json.
|
||||
|
||||
* Basic debugging commands (special permissions not required)
|
||||
* `$whatobj` and `$whatene` (non-proxy only): Tells you what the closest object or enemy spawn point is to your position, along with its coordinates and object or enemy ID. The full definition is also printed to the server's log.
|
||||
* `$qcheck <flag-num>` (non-proxy only): Show the value of a quest flag. If you're in a game, show the value of the flag in that game; if you're in the lobby, show the saved value of that quest flag for your character (BB only).
|
||||
* `$qgread <flag-num>` (non-proxy only): Show the value of a quest counter ("global flag").
|
||||
* `$sound <sound-id>`: Play the given sound (GC only).
|
||||
|
||||
* Restricted debugging commands (`$debug` permission required)
|
||||
* `$debug`: Enable debug mode. You need the DEBUG flag in your user account to use this command. Enabling debug does several things:
|
||||
* You'll be able to use the rest of the commands in this section.
|
||||
* You'll see in-game messages from the server when you take some actions, like killing enemies, opening boxes, or flipping switches.
|
||||
* You'll see the rare seed value and floor variations when you join a game.
|
||||
* You'll be placed into the last available slot in lobbies and games instead of the first, unless you're joining a BB solo-mode game.
|
||||
* You'll be able to join games with any PSO version, not only those for which cross-version play is normally enabled. See the "Cross-version play" section above for details on this.
|
||||
* `$readmem <address>`: Read 4 bytes from the given address and show you the values.
|
||||
* `$writemem <address> <data>`: Write data to the given address. Data is not required to be any specific size.
|
||||
* `$nativecall <address> [arg1 ...]` (GC only): Call a native function on your client. Only arguments passed in registers are supported; calling functions that take many arguments is not supported.
|
||||
* `$quest <number>` (non-proxy only): Load a quest by quest number. Can be used to load battle or challenge quests with only one player present. `$debug` is not required for this command if the specified quest has the AllowStartFromChatCommand field set in its metadata file.
|
||||
* `$qcall <function-id>`: Call a quest function on your client.
|
||||
* `$qset <flag-num>` or `$qclear <flag-num>`: Set or clear a quest flag for everyone in the game. If you're in the lobby and on BB, set or clear the saved value of a quest flag in your character file.
|
||||
* `$qgwrite <flag-num> <value>` (non-proxy only): Set the value of a quest counter ("global flag") for yourself.
|
||||
* `$qsync <reg-num> <value>`: Set a quest register's value for yourself only. `<reg-num>` should be either rXX (e.g. r60) or fXX (e.g. f60); if the latter, `<value>` is parsed as a floating-point value instead of as an integer.
|
||||
* `$qsyncall <reg-num> <value>`: Set a quest register's value for everyone in the game. `<reg-num>` should be either rXX (e.g. r60) or fXX (e.g. f60); if the latter, `<value>` is parsed as a floating-point value instead of as an integer.
|
||||
* `$swset [floor] <flag-num>` and `$swclear [floor] <flag-num>`: Set or clear a switch flag. If floor is not given, sets or clears the flag on your current floor.
|
||||
* `$swsetall`: Set all switch flags on your current floor. This unlocks all doors, disables all laser fences, triggers all light/poison switches, etc.
|
||||
* `$allrare`: Make all enemies and boxes drop their rare items every time.
|
||||
* `$gc` (non-proxy only): Send your own Guild Card to yourself.
|
||||
* `$sc <data>`: Send a command to yourself.
|
||||
* `$scp <data>`: Send a protected command to yourself.
|
||||
* `$ss <data>`: Send a command to the remote server (if in a proxy session) or to the game server.
|
||||
* `$sb <data>`: Send a command to yourself, and to the remote server or game server.
|
||||
* `$auction` (Episode 3 only): Bring up the CARD Auction menu, even if there are fewer than 4 players are in the game or you don't have a VIP card.
|
||||
* `$makeobj <type> [coords...] [angles...] [params...]`: Create a map object. This is only implemented for a few specific client versions. The type is an integer like `273` or `0x0107`. Coordinates are specified as e.g. `x:30 y:0 z:-25.5`; if coordinates are not specified, the object is created at the player's coordinates. Angles are specified as e.g. `r:0 p:0x1000 w:-0x400` (for roll, pitch, and yaw, respectively). Parameters are specified as e.g. `1:2.0 2:0.0 5:0x4000`; any unspecified parameters are set to zero. The object is only created for the calling player and is not added to the server's map state; if the object ever sends update commands (e.g. 6x0B), it will likely result in a disconnection.
|
||||
|
||||
* Personal state commands
|
||||
* `$arrow <color-id>`: Change your lobby arrow color. The color may be specified by number (0-12) or by name (red, blue, green, yellow, purple, cyan, orange, pink, white, white2, white3, or black).
|
||||
* `$secid <section-id>`: Set your override section ID. After running this command, any games you create will use your override section ID for rare drops instead of your character's actual section ID. If you're in a game and you are the leader of the game, this also immediately changes the item tables used by the server when creating items. To revert to your actual section id, run `$secid` with no name after it. On the proxy, this will not work if the remote server controls item drops. If the server does not allow cheat mode anywhere (that is, "CheatModeBehavior" is "Off" in config.json), this command does nothing.
|
||||
* `$rand <seed>`: Set your override random seed (specified as a 32-bit hex value). This will make any games you create use the given seed for rare enemies and item drops. On the proxy, this command can cause desyncs with other players in the same game, since they will not see the overridden random seed. To remove the override, run `$rand` with no arguments. If the server does not allow cheat mode anywhere (that is, "CheatModeBehavior" is "Off" in config.json), this command does nothing.
|
||||
* `$ln [name-or-type]`: Set the lobby number. Visible only to you. This command exists because some non-lobby maps can be loaded as lobbies with invalid lobby numbers. See the "GC lobby types" and "Ep3 lobby types" entries in the information menu for acceptable values here. Note that non-lobby maps do not have a lobby counter, so there's no way to exit the lobby without using either `$ln` again or `$exit`. On the game server, `$ln` reloads the lobby immediately; on the proxy, it doesn't take effect until you load another lobby yourself (which means you'll like have to use `$exit` to escape). Run this command with no argument to return to the default lobby.
|
||||
* `$swa`: Enable or disable switch assist. When enabled, the server will unlock two-player and four-player doors in non-quest games when you step on any of the required switches.
|
||||
* `$exit`: If you're in a lobby, send you to the main menu (which ends your proxy session, if you're in one). If you're in a game or spectator team, send you to the lobby (but does not end your proxy session if you're in one). Does nothing if you're in a non-Episode 3 game and no quest is in progress.
|
||||
* `$patch <name>`: Run a patch on your client. `<name>` must exactly match the name of a patch on the server.
|
||||
|
||||
* Character data commands (non-proxy only)
|
||||
* `$switchchar <slot>` (BB only): Switch to a different character from your account without logging out.
|
||||
* `$savechar <slot>`: Save your current character data on the server in the specified slot. See the [server-side saves section](#server-side-saves) for more details.
|
||||
* `$loadchar <slot>`: Load character data from the specified slot on the server, and replace your current character with it. See the [server-side saves section](#server-side-saves) for more details.
|
||||
* `$bbchar <username> <password> <slot>`: Save your current character data on the server in a different account's BB character slots. See the [server-side saves section](#server-side-saves) for more details.
|
||||
* `$checkchar [slot]`: Tells you basic information about a server-side character previously saved using `$savechar`. If `slot` is not given, tells you which slots are used and which are free.
|
||||
* `$deletechar <slot>`: Deletes a server-side character previously saved using `$savechar`.
|
||||
* `$edit <stat> <value>`: Modify your character data. See the [using $edit](#using-edit) section for details.
|
||||
|
||||
* Blue Burst player commands (non-proxy only)
|
||||
* `$bank [number]`: Switch your current bank, so you can access your other character's banks (if `number` is 1-4) or your shared account bank (if `number` is 0). If `number` is not given, switch back to your current character's bank.
|
||||
* `$save`: Save your character, system, and Guild Card data immediately. (By default, your character is saved every 60 seconds while online, and your account and Guild Card data are saved whenever they change.)
|
||||
|
||||
* Game state commands (non-proxy only)
|
||||
* `$maxlevel <level>`: Set the maximum level for players to join the current game. (This only applies when joining; if a player joins and then levels up past this level during the game, they are not kicked out, but won't be able to rejoin if they leave.)
|
||||
* `$minlevel <level>`: Set the minimum level for players to join the current game.
|
||||
* `$password <password>`: Set the game's join password. To unlock the game, run `$password` with nothing after it.
|
||||
* `$dropmode [mode]`: Change the way item drops behave in the current game. `mode` can be `none`, `client`, `shared`, `private`, or `duplicate`. If `mode` is not given, tells you the current drop mode without changing it. See the [item tables and drop modes section](#item-tables-and-drop-modes) for more information.
|
||||
* `$persist`: Enable or disable persistence for the current game. When persistence is on, the game will not be deleted when the last player leaves. The states of enemies, objects, and switches will be saved, and items left on the floor will not be deleted. (But if you're in the private or duplicate drop mode, items dropped by enemies are deleted - to make sure a certain item won't be deleted, you can pick it up and drop it again.) If the game is empty for too long (15 minutes by default), it is then deleted.
|
||||
|
||||
* Episode 3 commands (non-proxy only)
|
||||
* `$spec`: Toggle the allow spectators flag for Episode 3 games. If any players are spectating when this flag is disabled, they are sent back to the lobby.
|
||||
* `$inftime`: Toggle infinite-time mode. Must be used before starting a battle. If infinite-time mode is on, the overall and per-phase time limits will be disabled regardless of the values chosen during battle rules setup. After completing a battle, infinite-time mode is reset to the server's default value (which can be set in Episode3BehaviorFlags in config.json).
|
||||
* `$dicerange [d:L-H] [1:L-H] [a1:L-H] [d1:L-H]`: Set override dice ranges for the next battle. The min and max dice values from the rules setup menu always apply to the ATK dice, but you can specify a different range for the DEF dice with `d:2-4` (for example). The `1:` override applies to the 1-player team in a 2v1 game (so you would set the 2-player team's desired dice range in the rules menu). You can also specify the 1-player team's ATK and DEF ranges separately with the `a1:` and `d1:` overrides. Note that these ranges will only be used if the chosen map or quest does not override them.
|
||||
* `$stat <what>`: Show a statistic about your player or team in the current battle. `<what>` can be `duration`, `fcs-destroyed`, `cards-destroyed`, `damage-given`, `damage-taken`, `opp-cards-destroyed`, `own-cards-destroyed`, `move-distance`, `cards-set`, `fcs-set`, `attack-actions-set`, `techs-set`, `assists-set`, `defenses-self`, `defenses-ally`, `cards-drawn`, `max-attack-damage`, `max-combo`, `attacks-given`, `attacks-taken`, `sc-damage`, `damage-defended`, or `rank`.
|
||||
* `$surrender`: Cause your team to immediately lose the current battle. If your story character is already defeated, you can't surrender - only your teammate can.
|
||||
* `$saverec <name>`: Save the recording of the last battle.
|
||||
* `$playrec <name>`: Play a battle recording. This command creates a spectator team and plays the specified recording as if it were happening in real time. By default, playback will start immediately when the spectator team is ready; you can delay this to allow others to join by prepending a `!` to the recording name. In that case, using `$playrec` again (with no argument) within the spectator team will start playback.
|
||||
|
||||
* Cheat mode commands
|
||||
* `$cheat` (non-proxy only): Enable or disable cheat mode for the current game. All other cheat mode commands do nothing if cheat mode is disabled. By default, cheat mode is off in new games but can be enabled; there is an option in config.json that allows you to disable cheat mode entirely, or set it to on by default in new games. Cheat mode is always enabled on the proxy, unless cheat mode is disabled on the entire server.
|
||||
* `$infhp`: Enable or disable infinite HP mode. Applies to only you; does not affect other players. When enabled, one-hit KO attacks will still kill you, but on most versions of the game, the server will automatically revive you if you die. Infinite HP also automatically cures status ailments.
|
||||
* `$inftp`: Enable or disable infinite TP mode. Applies to only you; does not affect other players. Does not work on DCv1 or earlier versions.
|
||||
* `$fastkill`: Enable or disable fast kills. Applies to only you; does not affect other players. When enabled, the server will kill any enemy after you hit it once. Bosses are not affected by fast kills.
|
||||
* `$warpme <floor-id>` (or `$warp <floor-id>`): Warp yourself to the given floor.
|
||||
* `$warpall <floor-id>`: Warp everyone in the game to the given floor. You must be the leader to use this command, unless you're on the proxy.
|
||||
* `$next`: Warp yourself to the next floor.
|
||||
* `$item <desc>` (or `$i <desc>`): Create an item. `desc` may be a description of the item or a string of hex data specifying the item code. Item codes are 16 hex bytes; at least 2 bytes must be specified, and all unspecified bytes are zeroes. If you are on the proxy, you must not be using Blue Burst for this command to work. On the game server, this command works for all versions. Here are some examples to illustrate the syntax (nothing is case-sensitive, and everything except the item name itself is optional):
|
||||
* `$item Saber +5 0/10/25/0/10` (weapon with special, grind and attributes)
|
||||
* `$item ???? Draw Autogun` (untekked weapon with special; can have grind/attributes too, as above)
|
||||
* `$item SEALED J-SWORD K:2000` (weapon with kill count)
|
||||
* `$item ES APHEX ZALURE TWIN +200` (ES weapon must be prefixed with "ES"; name comes before special)
|
||||
* `$item DF FIELD +10DEF +20EVP +4` (armor with DFP bonus, EVP bonus, and slot count)
|
||||
* `$item RED MERGE +10DFP +20EVP` (shield; same as armor except without slot count)
|
||||
* `$item Knight/Power +9` (unit with specific modifier)
|
||||
* `$item Knight/Power++` (unit with normal modifier; ++/-- are +4/-4 and +/- are +2/-2)
|
||||
* `$item LIMITER K:1000` (sealed unit with kill count)
|
||||
* `$item Tapas PB:F,G,M&Y 120% 200IQ 5/195/0/0 green` (mag with PBs, synchro, IO, stats, and color)
|
||||
* `$item Trimate x10` (tool with stack size)
|
||||
* `$item Disk:Reverser` (technique disk without level)
|
||||
* `$item Disk:Razonde Lv.30` (technique disk with level)
|
||||
* `$item 1000 Meseta`
|
||||
* `$unset <index>` (non-proxy only): In an Episode 3 battle, removes one of your set cards from the field. `<index>` is the index of the set card as it appears on your screen - 1 is the card next to your SC's icon, 2 is the card to the right of 1, etc. This does not cause a Hunters-side SC to lose HP, as they normally do when their items are destroyed. You can also destroy the assist card set on yourself with `$unset 0`.
|
||||
* `$dropmode [mode]` (proxy only): Change the way item drops behave in the current game, if you are not on BB. Unlike the game server version of this command, using this on the proxy requires cheats to be enabled. This works by intercepting the drop requests sent to and from the leader. (So, if you are the leader and not using server drop mode on the remote server, it affects the entire game; otherwise, it affects only items generated by your actions.) `mode` can be `none` (no drops), `default` (normal drops), or `proxy` (use newserv's drop tables instead of the remote server's). If `mode` is not given, tells you the current drop mode without changing it.
|
||||
|
||||
* Aesthetic commands
|
||||
* `$event <event>`: Set the current holiday event in the current lobby. Holiday events are documented in the "Using $event" item in the information menu. If you're on the proxy, this applies to all lobbies and games you join, but only you will see the new event - other players will not.
|
||||
* `$allevent <event>` (non-proxy only): Set the current holiday event in all lobbies.
|
||||
* `$song <song-id>` (Episode 3 only): Play a specific song in the current lobby.
|
||||
|
||||
* Administration commands (non-proxy only)
|
||||
* `$ann <message>`: Send an announcement message. The message is sent as temporary on-screen text to all players in all games and lobbies. On BB, the message appears in the scrolling top bar.
|
||||
* `$ann!`, `$ann?`, `$ann?!`: Same as `$ann`, but with `?`, omits the sender's name, and with `!`, sends the message as a Simple Mail message instead of on-screen text.
|
||||
* `$silence <identifier>`: Silence a player (remove their ability to chat) or unsilence a player. The identifier may be the player's name or Guild Card number.
|
||||
* `$kick <identifier>`: Disconnect a player. The identifier may be the player's name or Guild Card number.
|
||||
* `$ban <duration> <identifier>`: Ban a player. The duration should be of the form `10m` (minutes), `10h` (hours), `10d` (days), `10w` (weeks), `10M` (months), or `10y` (years). (Numbers other than 10 may be used, of course.) As with `$kick`, the identifier may be the player's name or Guild Card number.
|
||||
|
||||
### Using $edit
|
||||
|
||||
The $edit command modifies your character data. This command doesn't work on V3 (GameCube/Xbox). If you are on V1 or V2 (DC or PC, not BB), your changes will be undone if you join a game - to save your changes, disconnect from the lobby.
|
||||
|
||||
Some subcommands are always available. They are:
|
||||
* `$edit mat reset power`: Clear your usage of power materials (BB only)
|
||||
* `$edit mat reset mind`: Clear your usage of mind materials (BB only)
|
||||
* `$edit mat reset evade`: Clear your usage of evade materials (BB only)
|
||||
* `$edit mat reset def`: Clear your usage of def materials (BB only)
|
||||
* `$edit mat reset luck`: Clear your usage of luck materials (BB only)
|
||||
* `$edit mat reset hp`: Clear your usage of HP materials (BB only)
|
||||
* `$edit mat reset tp`: Clear your usage of TP materials (BB only)
|
||||
* `$edit mat reset all`: Clear your usage of all materials except HP and TP (BB only)
|
||||
* `$edit mat reset every`: Clear your usage of all materials including HP and TP (BB only)
|
||||
* `$edit namecolor AARRGGBB`: Set your name color (AARRGGBB specified in hex)
|
||||
* `$edit language L`: Set your language (Generally only useful on BB; values for L: J = Japanese, E = English, G = German, F = French, S = Spanish, B = Simplified Chinese, T = Traditional Chinese, K = Korean)
|
||||
* `$edit name NAME`: Set your character name
|
||||
* `$edit npc NPC-NAME`: Set or remove an NPC skin on your character (use `none` to remove a skin). The NPC names are:
|
||||
* On all versions except DCv1 and early prototypes: `ninja`, `rico`, `sonic`, `knuckles`, `tails`
|
||||
* On GC, Xbox, and BB: `flowen`, `elly`
|
||||
* On BB only: `momoka`, `irene`, `guild`, `nurse`
|
||||
* `$edit secid SECID-NAME`: Set your section ID (cheat mode is required unless your character is Level 1)
|
||||
|
||||
The remaining subcommands are only available if cheat mode is enabled on the server. They are:
|
||||
* `$edit atp N`: Set your ATP to N until stats are updated (e.g. by leveling up)
|
||||
* `$edit mst N`: Set your MST to N until stats are updated
|
||||
* `$edit evp N`: Set your EVP to N until stats are updated
|
||||
* `$edit dfp N`: Set your DFP to N until stats are updated
|
||||
* `$edit ata N`: Set your ATA to N until stats are updated
|
||||
* `$edit lck N`: Set your LCK to N until stats are updated
|
||||
* `$edit hp N`: Set your HP to N until stats are updated
|
||||
* `$edit meseta N`: Set the amount of Meseta in your inventory
|
||||
* `$edit exp N`: Set your total amount of EXP (does not affect level)
|
||||
* `$edit level N`: Set your current level (recomputes stats, but does not affect EXP)
|
||||
* `$edit tech TECH-NAME LEVEL`: Set the level of one of your techniques
|
||||
|
||||
## REST API
|
||||
|
||||
newserv has an optional HTTP server that provides a way to programmatically get data from the server in realtime. This is intended for use with external integrations; for example, a web site could query this API to get the current player count to display on the home page.
|
||||
|
||||
The HTTP server is disabled by default, and you have to explicitly enable it in config.json if you want this functionality. **If you enable it, make sure that the HTTP port can't be accessed from the public Internet.** The API provides a lot of internal data about players and games, and it should only be accessed by programs that you've written or that you trust.
|
||||
|
||||
To enable the HTTP server, add a port number in the HTTPListen list in config.json. The HTTP server will listen on that port.
|
||||
|
||||
All returned data is JSON-encoded, and all request data (for POST requests) must also be JSON-encoded with the `Content-Type: application/json` header.
|
||||
|
||||
The HTTP server has the following endpoints:
|
||||
* `GET /`: Returns the server's build date and revision.
|
||||
* `GET /y/data/ep3-cards`: Returns the Episode 3 card definitions.
|
||||
* `GET /y/data/ep3-cards-trial`: Returns the Episode 3 Trial Edition card definitions.
|
||||
* `GET /y/data/common-tables`: Returns the parameters for generating common items (ItemPT files). This endpoint returns a lot of data and can be slow!
|
||||
* `GET /y/data/rare-tables`: Returns a list of rare table names.
|
||||
* `GET /y/data/rare-tables/<TABLE-NAME>` (for example, `/y/data/rare-tables/rare-table-v4`): Returns the contents of a rare item table.
|
||||
* `GET /y/data/quests`: Returns metadata about all available quests and quest categories.
|
||||
* `GET /y/data/config`: Returns the server's configuration file.
|
||||
* `GET /y/accounts`: Returns information about all registered accounts.
|
||||
* `GET /y/clients`: Returns information about all connected clients on the game server.
|
||||
* `GET /y/proxy-clients`: Returns information about all connected clients on the proxy.
|
||||
* `GET /y/lobbies`: Returns information about all lobbies and games.
|
||||
* `GET /y/server`: Returns information about the server.
|
||||
* `GET /y/summary`: Returns a summary of the server's state, connected clients, active games, and proxy sessions.
|
||||
* `WS /y/rare-drops/stream`: WebSocket endpoint that sends messages whenever an announceable rare item is dropped in any game. See below.
|
||||
* `POST /y/shell-exec`: Runs a server shell command. Input should be a JSON dict of e.g. `{"command": "announce hello"}`; response will be a JSON dict of `{"result": "<result text>"}` or an HTTP error.
|
||||
|
||||
### Rare drop stream endpoint
|
||||
|
||||
The `/y/rare-drops/stream` endpoint provides a way to implement a drop log in e.g. Discord. For every announceable rare item, a message is sent to all connected clients on this endpoint. (Announceable rare items are items for which an in-game or server-wide text message is sent announcing the find.)
|
||||
|
||||
Upon connecting, you'll get the message `{"ServerType": "newserv"}`. After that, when a rare item announcement is sent, you'll get a message like this:
|
||||
```
|
||||
{
|
||||
"PlayerAccountID", 12345,
|
||||
"PlayerName", "SONIC",
|
||||
"PlayerVersion", "GC_V3",
|
||||
"GameName", "ttf",
|
||||
"GameDropMode", "SERVER_PRIVATE",
|
||||
"ItemData", "03000000 00010000 00000000 (0021002C) 00000000",
|
||||
"ItemDescription", "Monomate x1",
|
||||
"NotifyGame", true,
|
||||
"NotifyServer", false,
|
||||
}
|
||||
```
|
||||
|
||||
# Non-server features
|
||||
|
||||
newserv has many CLI options, which can be used to access functionality other than the game server and proxy. Run `newserv help` to see a full list of the options and how to use each one.
|
||||
|
||||
The data formats that newserv can convert to/from are:
|
||||
|
||||
| Format | Encode/compress action | Decode/extract action |
|
||||
|-------------------------------------|---------------------------|------------------------------|
|
||||
| PRS compression | `compress-prs` | `decompress-prs` |
|
||||
| PR2/PRC compression | `compress-pr2` | `decompress-pr2` |
|
||||
| BC0 compression | `compress-bc0` | `decompress-bc0` |
|
||||
| Raw encrypted data | `encrypt-data` | `decrypt-data` |
|
||||
| Episode 3 command mask | `encrypt-trivial-data` | `decrypt-trivial-data` |
|
||||
| Challenge Mode rank text | `encrypt-challenge-data` | `decrypt-challenge-data` |
|
||||
| PSO DC quest file (.vms) | None | `decode-vms` |
|
||||
| PSO GC quest file (.gci) | None | `decode-gci` |
|
||||
| Download quest file (.dlq) | None | `decode-dlq` |
|
||||
| Server quest file (.qst) | `encode-qst` | `decode-qst` |
|
||||
| PSO DC save file (.vms) | `encrypt-vms-save` | `decrypt-vms-save` |
|
||||
| PSO PC save file | `encrypt-pc-save` | `decrypt-pc-save` |
|
||||
| PSO GC save file (.gci) | `encrypt-gci-save` | `decrypt-gci-save` |
|
||||
| PSO Xbox save file | None | `decrypt-xbox-save` |
|
||||
| PSO GC snapshot file | None | `decode-gci-snapshot` |
|
||||
| Quest script (.bin) | `assemble-quest-script` | `disassemble-quest-script` |
|
||||
| Quest map (.dat) | None | `disassemble-quest-map` |
|
||||
| AFS archive (.afs) | None | `extract-afs` |
|
||||
| BML archive (.bml) | None | `extract-bml` |
|
||||
| PPK archive (.ppk) | None | `extract-ppk` |
|
||||
| GSL archive (.gsl) | `generate-gsl` | `extract-gsl` |
|
||||
| GVM texture (.gvm) | `encode-gvm` | None |
|
||||
| Bitmap font (.fon) | `encode-bitmap-font` | `decode-bitmap-font` |
|
||||
| Text archive | `encode-text-archive` | `decode-text-archive` |
|
||||
| Unicode text set | `encode-unicode-text-set` | `decode-unicode-text-set` |
|
||||
| Word Select data set | None | `decode-word-select-set` |
|
||||
| Set data table | None | `disassemble-set-data-table` |
|
||||
| Rare item table (AFS/GSL/JSON/HTML) | `convert-rare-item-set` | `convert-rare-item-set` |
|
||||
|
||||
There are several actions that don't fit well into the table above, which let you do other things:
|
||||
|
||||
* Compute the decompressed size of compressed PRS data without decompressing it (`prs-size`)
|
||||
* Find the likely round1 or round2 seed for a corrupt save file (`salvage-gci`)
|
||||
* Run a brute-force search for a decryption seed (`find-decryption-seed`)
|
||||
* Format Episode 3 game data in a human-readable manner (`show-ep3-maps`, `show-ep3-cards`, `generate-ep3-cards-html`)
|
||||
* Format Blue Burst battle parameter files in a human-readable manner (`show-battle-params`)
|
||||
* Convert item data to a human-readable description, or vice versa (`describe-item`)
|
||||
* Show the server's item and level tables (`show-item-tables`, `show-level-tables`)
|
||||
* Connect to another PSO server and pretend to be a client (`cat-client`)
|
||||
* Generate or describe DC serial numbers (`generate-dc-serial-number`, `inspect-dc-serial-number`)
|
||||
|
||||
# Docker
|
||||
Docker is new and mostly unsupported at this time. However, here are some best-effort steps to build and run in a docker container on Ubuntu Linux.
|
||||
Tested on Ubuntu 22.04.4 LTS.
|
||||
Note: You cannot have anything except this docker container using port 53 (DNS) on your server.
|
||||
|
||||
Install prerequisites
|
||||
```
|
||||
sudo apt install -y git
|
||||
sudo apt install -y cmake. ## minimum version is 3.10. Check installed version with "cmake --version"
|
||||
```
|
||||
|
||||
Clone repository
|
||||
```
|
||||
cd ~
|
||||
git clone https://github.com/fuzziqersoftware/newserv/
|
||||
cd ~/newserv
|
||||
```
|
||||
|
||||
Build newserv. This will take a while. Don't forget the period at the end!
|
||||
```
|
||||
sudo docker build -t newserv .
|
||||
```
|
||||
|
||||
Create persistent directories. Assuming you want to store the persistent data in your home directory
|
||||
```
|
||||
mkdir ~/newservPersist
|
||||
mkdir ~/newservPersist/players
|
||||
mkdir ~/newservPersist/teams
|
||||
mkdir ~/newservPersist/licenses
|
||||
```
|
||||
|
||||
Copy config file to config dir
|
||||
```
|
||||
cp ~/newserv/system/config.example.json ~/newservPersist/config.json
|
||||
```
|
||||
|
||||
Edit config.json
|
||||
```
|
||||
nano ~/newservPersist/config.json
|
||||
```
|
||||
Pro tip:
|
||||
Set "LocalAddress" to the static, LAN IP address of your server. If your server LAN IP is "192.168.0.10":
|
||||
"LocalAddress": "192.168.0.10",
|
||||
|
||||
Set "ExternalAddress" to the WAN IP address of your network. If your WAN IP is "8.8.8.8":
|
||||
"ExternalAddress": "8.8.8.8",
|
||||
|
||||
For Dolphin > Settings. Set SP1 to "Broadband Adapter (HLE)" Click [...] next to this, and set the DNS to the IP address of your server. Then start the game. Changes will not take affect if the game is running.
|
||||
|
||||
Docker run. Remember to change /home/changeme/newservPersist to your persistent directory. Do not use aliases such as '~'
|
||||
```
|
||||
docker run --name newserv -p 53:53/udp -p 5100:5100 -p 5110:5110 -p 5111:5111 -p 5112:5112 -p 9064:9064 -p 9100:9100 -p 9103:9103 -p 9300:9300 -p 11000:11000 -p 12000:12000 -p 12004:12004 -p 12005:12005 -v /etc/localtime:/etc/localtime:ro -v /home/changeme/newservPersist/config.json:/newserv/system/config.json -v /home/changeme/newservPersist/players:/newserv/system/players -v /home/changeme/newservPersist/teams:/newserv/system/teams -v /home/changeme/newservPersist/licenses:/newserv/system/licenses --restart no newserv:latest
|
||||
```
|
||||
|
||||
Docker run host network mode. Remember to change /home/changeme/newservPersist to your persistent directory. Do not use aliases such as '~'
|
||||
```
|
||||
docker run --net host --name newserv -v /etc/localtime:/etc/localtime:ro -v /home/changeme/newservPersist/config.json:/newserv/system/config.json -v /home/changeme/newservPersist/players:/newserv/system/players -v /home/changeme/newservPersist/teams:/newserv/system/teams -v /home/changeme/newservPersist/licenses:/newserv/system/licenses --restart no newserv:latest
|
||||
```
|
||||
|
||||
Docker compose. Remember to change /home/changeme/newservPersist to your persistent directory. Do not use aliases such as '~'
|
||||
```
|
||||
name: psonewserv
|
||||
services:
|
||||
newserv:
|
||||
container_name: newserv
|
||||
ports:
|
||||
- 53:53/udp
|
||||
- 5100:5100
|
||||
- 5110:5110
|
||||
- 5111:5111
|
||||
- 5112:5112
|
||||
- 9064:9064
|
||||
- 9100:9100
|
||||
- 9103:9103
|
||||
- 9300:9300
|
||||
- 11000:11000
|
||||
- 12000:12000
|
||||
- 12004:12004
|
||||
- 12005:12005
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /home/changeme/newservPersist/config.json:/newserv/system/config.json
|
||||
- /home/changeme/newservPersist/players:/newserv/system/players
|
||||
- /home/changeme/newservPersist/teams:/newserv/system/teams
|
||||
- /home/changeme/newservPersist/licenses:/newserv/system/licenses
|
||||
restart: no ## Set to whatever you want.
|
||||
image: newserv:latest
|
||||
```
|
||||
Docker compose host network mode. Remember to change /home/changeme/newservPersist to your persistent directory. Do not use aliases such as '~'
|
||||
```
|
||||
name: psonewserv
|
||||
services:
|
||||
newserv:
|
||||
container_name: newserv
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /home/changeme/newservPersist/config.json:/newserv/system/config.json
|
||||
- /home/changeme/newservPersist/players:/newserv/system/players
|
||||
- /home/changeme/newservPersist/teams:/newserv/system/teams
|
||||
- /home/changeme/newservPersist/licenses:/newserv/system/licenses
|
||||
restart: no ## Set to whatever you want.
|
||||
network_mode: host
|
||||
image: newserv:latest
|
||||
```
|
||||
Executable
+67
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Callable
|
||||
|
||||
|
||||
def filter_directory(dir: str, predicate: Callable[[str], bool]):
|
||||
for filename in os.listdir(dir):
|
||||
if not predicate(filename):
|
||||
path = os.path.join(dir, filename)
|
||||
if os.path.isfile(path):
|
||||
os.remove(path)
|
||||
else:
|
||||
shutil.rmtree(path)
|
||||
|
||||
|
||||
def main():
|
||||
print("Deleting existing release directory")
|
||||
if os.path.exists("release"):
|
||||
shutil.rmtree("release")
|
||||
if os.path.exists("release.zip"):
|
||||
os.remove("release.zip")
|
||||
os.mkdir("release")
|
||||
|
||||
print("Adding executables")
|
||||
shutil.copy("newserv", "release/newserv-macos")
|
||||
shutil.copy("newserv.exe", "release/newserv-windows.exe")
|
||||
shutil.copy("README.md", "release/README.md")
|
||||
|
||||
print("Adding system directory")
|
||||
shutil.copytree("system", "release/system")
|
||||
|
||||
print("Removing instance-based and temporary files")
|
||||
filter_directory(
|
||||
"release/system",
|
||||
lambda filename: (not filename.endswith(".json"))
|
||||
or filename == "config.example.json",
|
||||
)
|
||||
filter_directory(
|
||||
"release/system/ep3", lambda filename: not filename.startswith("cardtex")
|
||||
)
|
||||
filter_directory(
|
||||
"release/system/client-functions",
|
||||
lambda filename: filename not in ("Debug-Private", "FastLoading", "notes.txt"),
|
||||
)
|
||||
filter_directory("release/system/dol", lambda filename: False)
|
||||
filter_directory("release/system/ep3/banners", lambda filename: False)
|
||||
filter_directory("release/system/ep3/battle-records", lambda filename: False)
|
||||
filter_directory("release/system/licenses", lambda filename: False)
|
||||
filter_directory("release/system/players", lambda filename: False)
|
||||
filter_directory(
|
||||
"release/system/quests",
|
||||
lambda filename: filename not in ("private", "includes"),
|
||||
)
|
||||
filter_directory("release/system/teams", lambda filename: filename == "base.json")
|
||||
subprocess.check_call(["find", "release", "-name", ".DS_Store", "-delete"])
|
||||
subprocess.check_call(["find", "release", "-name", "*.WIP-s", "-delete"])
|
||||
|
||||
print("Setting up configuration")
|
||||
os.rename("release/system/config.example.json", "release/system/config.json")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -81,12 +81,14 @@ Disable item equip restrictions ("God of equip")
|
||||
3OJ5 => 041050D4 38000005
|
||||
3OJT => 0415BF50 38000005
|
||||
3OP0 => 041052D4 38000005
|
||||
59NJ => 005C9F35 E9A7000000
|
||||
59NL => 005C9F31 E9A7000000
|
||||
|
||||
All items visible in Pioneer 2
|
||||
3OE1 => 04102D88 38600000
|
||||
|
||||
Mags visible in Pioneer 2
|
||||
59NJ => 005D8F27 EB04
|
||||
59NL => 005D8F4B EB04
|
||||
|
||||
Disable pause menu background + offset
|
||||
@@ -94,6 +96,9 @@ Disable pause menu background + offset
|
||||
0428735C 4800000C
|
||||
3OE2 => 0424CED8 48000370
|
||||
042887D8 4800000C
|
||||
59NJ => 00719C58 9090
|
||||
00733C57 9090
|
||||
00733ABE 90E9
|
||||
59NL => 00719B54 9090
|
||||
00733BA7 9090
|
||||
00733A0E 90E9
|
||||
@@ -637,6 +642,8 @@ Fast tekker (skips wind-up jingle)
|
||||
0023EF77 jmp +0x0A
|
||||
4OPU => 0023F14C mov dword [ebp + 0x14C], 1
|
||||
0023F167 jmp +0x0A
|
||||
59NJ => 006DA14B mov dword [edi + 0x14C], 1
|
||||
006DA168 jmp +0x0B
|
||||
59NL => 006DA113 mov dword [edi + 0x14C], 1
|
||||
006DA130 jmp +0x0B
|
||||
|
||||
@@ -973,6 +980,8 @@ Override Challenge mode random enemy location tables limit
|
||||
4OEU => 002E742C XXXXXXXX (count as little-endian dword)
|
||||
4OPD => 002E720C XXXXXXXX (count as little-endian dword)
|
||||
4OPU => 002E745C XXXXXXXX (count as little-endian dword)
|
||||
59NJ => 0080FA3F XXXXXXXX (count * 4 as little-endian dword)
|
||||
0080FA58 XXXXXXXX (count as little-endian dword)
|
||||
59NL => 0080ECB7 XXXXXXXX (count * 4 as little-endian dword)
|
||||
0080ECD0 XXXXXXXX (count as little-endian dword)
|
||||
|
||||
@@ -1074,3 +1083,37 @@ Enable quest board menu in free play (for use with the above code)
|
||||
3OJ3 => 04262E44 38600001
|
||||
3OJ4 => 04263EB8 38600001
|
||||
3OP0 => 0426374C 38600001
|
||||
|
||||
All classes' footsteps sound like RAcast's
|
||||
(Change the 2 in 38600002 to 0 for human/Newman, 1 for lighter androids, or 3 if you want to be annoyed)
|
||||
3OE0 => 041B3ED0 38600002
|
||||
041B3ED4 4E800020
|
||||
3OE1 => 041B3ED0 38600002
|
||||
041B3ED4 4E800020
|
||||
3OE2 => 041B4068 38600002
|
||||
041B406C 4E800020
|
||||
3OJ2 => 041B3AE4 38600002
|
||||
041B3AE8 4E800020
|
||||
3OJ3 => 041B3F38 38600002
|
||||
041B3F3C 4E800020
|
||||
3OJ4 => 041B552C 38600002
|
||||
041B5530 4E800020
|
||||
3OJ5 => 041B4004 38600002
|
||||
041B4008 4E800020
|
||||
3OJT => 0420A120 38600002
|
||||
0420A124 4E800020
|
||||
3OP0 => 041B4524 38600002
|
||||
041B4528 4E800020
|
||||
3SE0 => 040D0378 38600002
|
||||
040D037C 4E800020
|
||||
3SJ0 => 040D0394 38600002
|
||||
040D0398 4E800020
|
||||
3SJT => 040D431C 38600002
|
||||
040D4320 4E800020
|
||||
3SP0 => 040D07BC 38600002
|
||||
040D07C0 4E800020
|
||||
|
||||
Rappy size modifier
|
||||
3OE1 => 040C1E24 48000020 // Disable flag check in render
|
||||
045D0718 40800000 // X/Z scale as float (here, 4.0)
|
||||
045D071C 40800000 // Y scale as float (here, 4.0)
|
||||
|
||||
@@ -538,8 +538,8 @@ fparam4 = TODO: 3OE1:802D7490; NNF: Jump cooldown time (Higher value = less wait
|
||||
iparam1 = TODO: 3OE1:802D7484
|
||||
|
||||
MOVEMENT DATA 40 (MORFOS)
|
||||
fparam1 = TODO: 3OE1:80335A98, 3OE1:80335DBC; NNF: Laser speed (also affects hitbox property - needs retest with others for opinion).
|
||||
fparam2 = TODO: 3OE1:80335ADC; NNF: Laser damage.
|
||||
fparam1 = laser speed; hitbox radius is fparam1 * 1.5
|
||||
fparam2 = laser damage
|
||||
iparam1 = TODO: 3OE1:80332298, 3OE1:803321C4; NNF: Firing rate of regular laser attack. Laser attack when aggressive (charging) is unaffected.
|
||||
iparam2 = TODO: 3OE1:8033161C, 3OE1:8033192C, 3OE1:80331B4C, 3OE1:80331D00, 3OE1:80331FA0; NNF: Speed at which Morphos spins after firing laser.
|
||||
iparam3 = TODO: 3OE1:80331F04; NNF: Interval in frames of attacks
|
||||
|
||||
+2
-6
@@ -14,11 +14,9 @@ struct BMLHeaderT {
|
||||
parray<uint8_t, 0x04> unknown_a1;
|
||||
U32T<BE> num_entries;
|
||||
parray<uint8_t, 0x38> unknown_a2;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws_be__(BMLHeaderT, 0x40);
|
||||
using BMLHeader = BMLHeaderT<false>;
|
||||
using BMLHeaderBE = BMLHeaderT<true>;
|
||||
check_struct_size(BMLHeader, 0x40);
|
||||
check_struct_size(BMLHeaderBE, 0x40);
|
||||
|
||||
template <bool BE>
|
||||
struct BMLHeaderEntryT {
|
||||
@@ -29,11 +27,9 @@ struct BMLHeaderEntryT {
|
||||
U32T<BE> compressed_gvm_size;
|
||||
U32T<BE> decompressed_gvm_size;
|
||||
parray<uint8_t, 0x0C> unknown_a2;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws_be__(BMLHeaderEntryT, 0x40);
|
||||
using BMLHeaderEntry = BMLHeaderEntryT<false>;
|
||||
using BMLHeaderEntryBE = BMLHeaderEntryT<true>;
|
||||
check_struct_size(BMLHeaderEntry, 0x40);
|
||||
check_struct_size(BMLHeaderEntryBE, 0x40);
|
||||
|
||||
template <bool BE>
|
||||
void BMLArchive::load_t() {
|
||||
|
||||
+68
-18
@@ -439,12 +439,14 @@ ChatCommandDefinition cc_bank(
|
||||
});
|
||||
|
||||
static asio::awaitable<void> server_command_bbchar_savechar(const Args& a, bool is_bb_conversion) {
|
||||
// TODO: We could support this in proxy sessions; we'd just have to handle the 61/30 correctly
|
||||
a.check_is_proxy(false);
|
||||
// Allow $savechar in proxy sessions for character migration/testing.
|
||||
// Keep $bbchar blocked in proxy sessions because it writes to BB account slots.
|
||||
if (is_bb_conversion) {
|
||||
a.check_is_proxy(false);
|
||||
}
|
||||
a.check_is_game(false);
|
||||
|
||||
auto s = a.c->require_server_state();
|
||||
auto l = a.c->require_lobby();
|
||||
|
||||
if (is_bb_conversion && is_ep3(a.c->version())) {
|
||||
throw precondition_failed("$C6Episode 3 players\ncannot be converted\nto BB format");
|
||||
@@ -481,9 +483,14 @@ static asio::awaitable<void> server_command_bbchar_savechar(const Args& a, bool
|
||||
dest_account = a.c->login->account;
|
||||
}
|
||||
|
||||
// If the client isn't BB, request the player info. (If they are BB, the server already has it)
|
||||
// In direct sessions, request player info from the client.
|
||||
// In proxy sessions, don't use the 61/30 request path; use the server-side
|
||||
// character state already tracked for the proxied client.
|
||||
GetPlayerInfoResult ch;
|
||||
if (a.c->version() == Version::BB_V4) {
|
||||
if ((a.c->version() == Version::BB_V4) || a.c->proxy_session) {
|
||||
if (is_ep3(a.c->version())) {
|
||||
throw precondition_failed("$C6Proxy savechar for\nEpisode 3 is not\nimplemented yet");
|
||||
}
|
||||
ch.character = a.c->character_file();
|
||||
ch.is_full_info = true;
|
||||
} else {
|
||||
@@ -1528,14 +1535,32 @@ ChatCommandDefinition cc_ln(
|
||||
ChatCommandDefinition cc_loadchar(
|
||||
{"$loadchar"},
|
||||
+[](const Args& a) -> asio::awaitable<void> {
|
||||
a.check_is_proxy(false);
|
||||
if (a.c->proxy_session && (a.c->version() == Version::BB_V4)) {
|
||||
throw precondition_failed("$C6This command cannot\nbe used in proxy\nsessions in BB games");
|
||||
}
|
||||
a.check_is_game(false);
|
||||
if (a.check_permissions && a.c->login->account->check_flag(Account::Flag::IS_SHARED_ACCOUNT)) {
|
||||
throw precondition_failed("$C7This command cannot\nbe used on a shared\naccount");
|
||||
}
|
||||
|
||||
auto s = a.c->require_server_state();
|
||||
auto l = a.c->require_lobby();
|
||||
shared_ptr<Lobby> l;
|
||||
if (a.c->proxy_session) {
|
||||
if (!a.c->proxy_session->is_in_lobby) {
|
||||
throw precondition_failed("$C6This command can\nonly be used from\nthe lobby");
|
||||
}
|
||||
} else {
|
||||
l = a.c->require_lobby();
|
||||
}
|
||||
|
||||
auto send_proxy_lobby_refresh = [&]() {
|
||||
auto temp_l = make_shared<Lobby>(s, 0xFFFFFFFF, false);
|
||||
temp_l->block = 1;
|
||||
temp_l->event = a.c->proxy_session ? a.c->proxy_session->lobby_event : 0;
|
||||
temp_l->leader_id = a.c->lobby_client_id;
|
||||
temp_l->clients.at(a.c->lobby_client_id) = a.c;
|
||||
send_join_lobby(a.c, temp_l);
|
||||
};
|
||||
|
||||
size_t index = stoull(a.text, nullptr, 0) - 1;
|
||||
if (index >= s->num_backup_character_slots) {
|
||||
@@ -1568,7 +1593,7 @@ ChatCommandDefinition cc_loadchar(
|
||||
throw precondition_failed("Can\'t load character\ndata on this game\nversion");
|
||||
}
|
||||
|
||||
auto send_set_extended_player_info = [&a, &s]<typename CharT>(const CharT& char_file) -> asio::awaitable<void> {
|
||||
auto send_set_extended_player_info = [&a, &s, &send_proxy_lobby_refresh]<typename CharT>(const CharT& char_file) -> asio::awaitable<void> {
|
||||
co_await prepare_client_for_patches(a.c);
|
||||
try {
|
||||
auto fn = s->function_code_index->get_patch("SetExtendedPlayerInfo", a.c->specific_version);
|
||||
@@ -1577,6 +1602,8 @@ ChatCommandDefinition cc_loadchar(
|
||||
if (l) {
|
||||
send_player_leave_notification(l, a.c->lobby_client_id);
|
||||
s->send_lobby_join_notifications(l, a.c);
|
||||
} else if (a.c->proxy_session) {
|
||||
send_proxy_lobby_refresh();
|
||||
}
|
||||
} catch (const exception& e) {
|
||||
a.c->log.warning_f("Failed to set extended player info: {}", e.what());
|
||||
@@ -1611,11 +1638,15 @@ ChatCommandDefinition cc_loadchar(
|
||||
}
|
||||
|
||||
} else {
|
||||
// On v1 and v2, the client will assign its character data from the lobby join command, so it suffices to just
|
||||
// resend the join notification.
|
||||
auto s = a.c->require_server_state();
|
||||
send_player_leave_notification(l, a.c->lobby_client_id);
|
||||
s->send_lobby_join_notifications(l, a.c);
|
||||
// On PC V2 and older DC versions, the client will assign its character data
|
||||
// from the lobby join command, so resend a lobby join notification.
|
||||
if (a.c->proxy_session) {
|
||||
send_proxy_lobby_refresh();
|
||||
} else {
|
||||
auto s = a.c->require_server_state();
|
||||
send_player_leave_notification(l, a.c->lobby_client_id);
|
||||
s->send_lobby_join_notifications(l, a.c);
|
||||
}
|
||||
}
|
||||
co_return;
|
||||
});
|
||||
@@ -2227,6 +2258,15 @@ ChatCommandDefinition cc_fastkill(
|
||||
}
|
||||
co_return;
|
||||
});
|
||||
ChatCommandDefinition cc_allrare(
|
||||
{"$allrare"},
|
||||
+[](const Args& a) -> asio::awaitable<void> {
|
||||
a.check_debug_enabled();
|
||||
a.c->toggle_flag(Client::Flag::ALL_RARES_ENABLED);
|
||||
send_text_message_fmt(
|
||||
a.c, "$C6All-rares {}", a.c->check_flag(Client::Flag::ALL_RARES_ENABLED) ? "enabled" : "disabled");
|
||||
co_return;
|
||||
});
|
||||
|
||||
ChatCommandDefinition cc_rand(
|
||||
{"$rand"},
|
||||
@@ -2349,14 +2389,18 @@ ChatCommandDefinition cc_saverec(
|
||||
co_return;
|
||||
});
|
||||
|
||||
static asio::awaitable<void> command_send_command(const Args& a, bool to_client, bool to_server) {
|
||||
static asio::awaitable<void> command_send_command(const Args& a, bool to_client, bool to_server, bool send_protected) {
|
||||
if (!a.c->proxy_session) {
|
||||
a.check_debug_enabled();
|
||||
}
|
||||
string data = phosg::parse_data_string(a.text);
|
||||
data.resize((data.size() + 3) & (~3));
|
||||
if (to_client) {
|
||||
a.c->channel->send(data);
|
||||
if (send_protected) {
|
||||
co_await send_protected_command(a.c, data.data(), data.size(), false);
|
||||
} else {
|
||||
a.c->channel->send(data);
|
||||
}
|
||||
}
|
||||
if (to_server) {
|
||||
if (a.c->proxy_session) {
|
||||
@@ -2371,13 +2415,19 @@ static asio::awaitable<void> command_send_command(const Args& a, bool to_client,
|
||||
ChatCommandDefinition cc_sb(
|
||||
{"$sb"},
|
||||
+[](const Args& a) -> asio::awaitable<void> {
|
||||
return command_send_command(a, true, true);
|
||||
return command_send_command(a, true, true, false);
|
||||
});
|
||||
|
||||
ChatCommandDefinition cc_sc(
|
||||
{"$sc"},
|
||||
+[](const Args& a) -> asio::awaitable<void> {
|
||||
return command_send_command(a, true, false);
|
||||
return command_send_command(a, true, false, false);
|
||||
});
|
||||
|
||||
ChatCommandDefinition cc_scp(
|
||||
{"$scp"},
|
||||
+[](const Args& a) -> asio::awaitable<void> {
|
||||
return command_send_command(a, true, false, true);
|
||||
});
|
||||
|
||||
ChatCommandDefinition cc_secid(
|
||||
@@ -2571,7 +2621,7 @@ ChatCommandDefinition cc_spec(
|
||||
ChatCommandDefinition cc_ss(
|
||||
{"$ss"},
|
||||
+[](const Args& a) -> asio::awaitable<void> {
|
||||
return command_send_command(a, false, true);
|
||||
return command_send_command(a, false, true, false);
|
||||
});
|
||||
|
||||
ChatCommandDefinition cc_stat(
|
||||
|
||||
+1
-3
@@ -40,11 +40,9 @@ struct ChoiceSearchConfigT {
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
} __attribute__((packed));
|
||||
} __packed_ws_be__(ChoiceSearchConfigT, 0x18);
|
||||
using ChoiceSearchConfig = ChoiceSearchConfigT<false>;
|
||||
using ChoiceSearchConfigBE = ChoiceSearchConfigT<true>;
|
||||
check_struct_size(ChoiceSearchConfig, 0x18);
|
||||
check_struct_size(ChoiceSearchConfigBE, 0x18);
|
||||
|
||||
struct ChoiceSearchCategory {
|
||||
struct Choice {
|
||||
|
||||
+3
-3
@@ -1008,9 +1008,9 @@ void Client::load_all_files() {
|
||||
auto stack_limits = s->item_stack_limits(this->version());
|
||||
|
||||
this->blocked_senders.clear();
|
||||
for (size_t z = 0; z < this->guild_card_data->blocked.size(); z++) {
|
||||
if (this->guild_card_data->blocked[z].present) {
|
||||
this->blocked_senders.emplace(this->guild_card_data->blocked[z].guild_card_number);
|
||||
for (size_t z = 0; z < this->guild_card_data->blocked_senders.size(); z++) {
|
||||
if (this->guild_card_data->blocked_senders[z].present) {
|
||||
this->blocked_senders.emplace(this->guild_card_data->blocked_senders[z].guild_card_number);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+5
-1
@@ -78,10 +78,12 @@ public:
|
||||
INFINITE_HP_ENABLED = 0x0000000040000000,
|
||||
INFINITE_TP_ENABLED = 0x0000000080000000,
|
||||
FAST_KILLS_ENABLED = 0x0000000100000000,
|
||||
ALL_RARES_ENABLED = 0x0000100000000000,
|
||||
DEBUG_ENABLED = 0x0000000200000000,
|
||||
ITEM_DROP_NOTIFICATIONS_1 = 0x0000000400000000,
|
||||
ITEM_DROP_NOTIFICATIONS_2 = 0x0000000800000000,
|
||||
HAS_ENEMY_DAMAGE_SYNC_PATCH = 0x0000001000000000, // Must be same as in EnemyDamageSync*.s
|
||||
HAS_PSO_PEEPS_XP_PATCH = 0x0000200000000000, // Must be same as in PSO Peeps XP patches
|
||||
|
||||
// Proxy option flags
|
||||
PROXY_SAVE_FILES = 0x0000002000000000,
|
||||
@@ -139,6 +141,7 @@ public:
|
||||
std::shared_ptr<Channel> channel;
|
||||
std::shared_ptr<PSOBBMultiKeyDetectorEncryption> bb_detector_crypt;
|
||||
ServerBehavior server_behavior;
|
||||
uint16_t listener_port = 0;
|
||||
std::unordered_map<std::string, std::function<void()>> disconnect_hooks;
|
||||
uint64_t ping_start_time = 0;
|
||||
|
||||
@@ -149,13 +152,14 @@ public:
|
||||
uint8_t override_lobby_event = 0xFF; // FF = no override
|
||||
uint8_t override_lobby_number = 0x80; // 80 = no override
|
||||
int64_t override_random_seed = -1;
|
||||
int8_t selected_blueballz_tier = -1; // -1 = normal lobby/game; 0..10 = requested Blueballz tier
|
||||
std::unique_ptr<Variations> override_variations;
|
||||
VectorXYZF pos;
|
||||
uint32_t floor = 0x0F;
|
||||
std::weak_ptr<Lobby> lobby;
|
||||
uint8_t lobby_client_id = 0;
|
||||
uint8_t lobby_arrow_color = 0;
|
||||
int64_t preferred_lobby_id = -1; // <0 = no preference
|
||||
int64_t preferred_lobby_id = -1; // <0 = none chosen
|
||||
|
||||
asio::steady_timer save_game_data_timer;
|
||||
asio::steady_timer send_ping_timer;
|
||||
|
||||
+254
-214
@@ -144,7 +144,7 @@ struct S_ServerInit_Patch_02 {
|
||||
} __packed_ws__(S_ServerInit_Patch_02, 0x48);
|
||||
|
||||
// 02 (C->S): Encryption started
|
||||
// No arguments
|
||||
// No arguments.
|
||||
|
||||
// 03: Invalid command
|
||||
|
||||
@@ -163,7 +163,7 @@ struct C_Login_Patch_04 {
|
||||
} __packed_ws__(C_Login_Patch_04, 0x6C);
|
||||
|
||||
// 05 (S->C): Disconnect
|
||||
// No arguments
|
||||
// No arguments.
|
||||
// This command is not used in the normal flow (described above). Generally the server should disconnect after sending
|
||||
// a 12 or 15 command instead of an 05.
|
||||
|
||||
@@ -203,10 +203,10 @@ struct S_EnterDirectory_Patch_09 {
|
||||
} __packed_ws__(S_EnterDirectory_Patch_09, 0x40);
|
||||
|
||||
// 0A (S->C): Exit directory
|
||||
// No arguments
|
||||
// No arguments.
|
||||
|
||||
// 0B (S->C): Start patch session and go to patch root directory
|
||||
// No arguments
|
||||
// No arguments.
|
||||
|
||||
// 0C (S->C): File checksum request
|
||||
|
||||
@@ -216,7 +216,7 @@ struct S_FileChecksumRequest_Patch_0C {
|
||||
} __packed_ws__(S_FileChecksumRequest_Patch_0C, 0x24);
|
||||
|
||||
// 0D (S->C): End of file checksum requests
|
||||
// No arguments
|
||||
// No arguments.
|
||||
|
||||
// 0E: Invalid command
|
||||
|
||||
@@ -229,7 +229,7 @@ struct C_FileInformation_Patch_0F {
|
||||
} __packed_ws__(C_FileInformation_Patch_0F, 0x0C);
|
||||
|
||||
// 10 (C->S): End of file information command list
|
||||
// No arguments
|
||||
// No arguments.
|
||||
|
||||
// 11 (S->C): Start file downloads
|
||||
|
||||
@@ -239,7 +239,7 @@ struct S_StartFileDownloads_Patch_11 {
|
||||
} __packed_ws__(S_StartFileDownloads_Patch_11, 0x08);
|
||||
|
||||
// 12 (S->C): End patch session successfully
|
||||
// No arguments
|
||||
// No arguments.
|
||||
|
||||
// 13 (S->C): Message box
|
||||
// Same format and usage as commands 1A/D5 on the game server (described below). On PSOBB, the message box appears in
|
||||
@@ -251,14 +251,13 @@ struct S_StartFileDownloads_Patch_11 {
|
||||
// 14 (S->C): Reconnect
|
||||
// Same format and usage as command 19 on the game server (described below), except the port field is big-endian.
|
||||
|
||||
template <typename PortT>
|
||||
template <bool BE>
|
||||
struct S_ReconnectT {
|
||||
be_uint32_t address = 0;
|
||||
PortT port = 0;
|
||||
U16T<BE> port = 0;
|
||||
le_uint16_t unused = 0;
|
||||
} __attribute__((packed));
|
||||
using S_Reconnect_Patch_14 = S_ReconnectT<be_uint16_t>;
|
||||
check_struct_size(S_Reconnect_Patch_14, 0x08);
|
||||
} __packed_ws_be__(S_ReconnectT, 0x08);
|
||||
using S_Reconnect_Patch_14 = S_ReconnectT<false>;
|
||||
|
||||
// 15 (S->C): Login failure
|
||||
// No arguments. The client shows a message like "Incorrect game ID or password" and disconnects.
|
||||
@@ -293,10 +292,10 @@ struct SC_TextHeader_01_06_11_B0_EE {
|
||||
// Command 17 should be used instead for the first connection.
|
||||
// All commands after this command will be encrypted with PSO V2 encryption on DC, PC, and GC Episodes 1&2 Trial
|
||||
// Edition, or PSO V3 encryption on other V3 versions. The (encrypted) response depends on the client's version:
|
||||
// - DC NTE clients will respond with 8B.
|
||||
// - DC 11/2000 and DCv1 clients will respond with 93.
|
||||
// - DCv2, PCv2, and GC NTE clients will respond with an 9A, 9D, or 9E.
|
||||
// - V3 clients will respond with 9A or 9E command.
|
||||
// - DC NTE clients will respond with an 8B command.
|
||||
// - DC 11/2000 and DCv1 clients will respond with a 93 command.
|
||||
// - DCv2, PCv2, and GC NTE clients will respond with a 9A, 9D, or 9E command.
|
||||
// - V3 clients will respond with a 9A or 9E command.
|
||||
// The copyright field in the below structure must contain "DreamCast Lobby Server. Copyright SEGA Enterprises. 1999".
|
||||
// (The above text is required on all versions that use this command, including those versions that don't run on the
|
||||
// DreamCast.)
|
||||
@@ -494,16 +493,16 @@ struct S_MenuItemT {
|
||||
// - On BB, 0x40/0x41 mean Episodes 1/2 as on GC, and 0x43 means Episode 4.
|
||||
uint8_t episode = 0;
|
||||
// Flags (01 and 02 are used for all menus; the rest are only used for the game menu):
|
||||
// 01 = Send name? (client sends the name field in the 10 command if this item is chosen, but it's blank)
|
||||
// 02 = Locked (lock icon appears in the menu; player is prompted for a password if they choose this game)
|
||||
// 04 = In battle (Episode 3 only; a sword icon appears in the menu)
|
||||
// 04 = Disabled (BB only; used for solo games)
|
||||
// 10 = Is battle mode
|
||||
// 20 = Is challenge mode
|
||||
// 40 = Is v2 only (DCv2/PC); game name renders in orange
|
||||
// 40 = Is Episode 1 (V3/BB)
|
||||
// 80 = Is Episode 2 (V3/BB)
|
||||
// C0 = Is Episode 4 (BB)
|
||||
// 01 = Send name? (client sends the name field in the 10 command if this item is chosen, but it's blank)
|
||||
// 02 = Locked (lock icon appears in the menu; player is prompted for a password if they choose this game)
|
||||
// 04 = In battle (Episode 3 only; a sword icon appears in the menu)
|
||||
// 04 = Disabled (BB only; used for solo games)
|
||||
// 10 = Is battle mode
|
||||
// 20 = Is challenge mode
|
||||
// 40 = Is v2 only (DCv2/PC); game name renders in orange
|
||||
// 40 = Is Episode 1 (V3/BB)
|
||||
// 80 = Is Episode 2 (V3/BB)
|
||||
// C0 = Is Episode 4 (BB)
|
||||
uint8_t flags = 0;
|
||||
} __attribute__((packed));
|
||||
using S_MenuItem_PC_BB_08 = S_MenuItemT<TextEncoding::UTF16>;
|
||||
@@ -513,7 +512,7 @@ check_struct_size(S_MenuItem_DC_V3_08_Ep3_E6, 0x1C);
|
||||
|
||||
// 08 (C->S): Request game list
|
||||
// Internal name: SndGameList
|
||||
// No arguments
|
||||
// No arguments.
|
||||
|
||||
// 08 (S->C): Game list
|
||||
// Internal name: RcvGameList
|
||||
@@ -530,6 +529,7 @@ struct C_MenuItemInfoRequest_09 {
|
||||
le_uint32_t item_id = 0;
|
||||
} __packed_ws__(C_MenuItemInfoRequest_09, 8);
|
||||
|
||||
// 0A: Invalid command
|
||||
// 0B: Invalid command
|
||||
|
||||
// 0C (C->S): Create game (DCv1)
|
||||
@@ -709,10 +709,10 @@ struct C_WriteFileConfirmation_V3_BB_13_A7 {
|
||||
// Internal name: RcvPsoRegistConnectV2
|
||||
// Same format as 02 command, but a different copyright: "DreamCast Port Map. Copyright SEGA Enterprises. 1999"
|
||||
// The response depends on the client's version:
|
||||
// - DC NTE will respond with 8B.
|
||||
// - DC 11/2000 and DCv1 will respond with 90.
|
||||
// - DCv2, PCv2, and GC NTE clients will respond with 9A or 9D.
|
||||
// - V3 (GC/Xbox) clients will respond with a DB command when they receive a 17 command in any online
|
||||
// - DC NTE will respond with an 8B command.
|
||||
// - DC 11/2000 and DCv1 will respond with a 90 command.
|
||||
// - DCv2, PCv2, and GC NTE clients will respond with a 9A or 9D command.
|
||||
// - V3 (GC/Xbox) clients will respond with a DB command (but see the notes on DB for Xbox clients).
|
||||
|
||||
// 18 (S->C): Account verification result (PC/V3)
|
||||
// Behaves exactly the same as 9A (S->C). No arguments except header.flag.
|
||||
@@ -723,8 +723,7 @@ struct C_WriteFileConfirmation_V3_BB_13_A7 {
|
||||
// connection; the server should send an appropriate command to enable it when the client connects. PSO Xbox seems to
|
||||
// ignore the address field, which makes sense given its networking architecture.
|
||||
|
||||
using S_Reconnect_19 = S_ReconnectT<le_uint16_t>;
|
||||
check_struct_size(S_Reconnect_19, 8);
|
||||
using S_Reconnect_19 = S_ReconnectT<false>;
|
||||
|
||||
// Sylverant implements an IPv6 version of this command, but it's not obvious why. IPv6 technically did exist as a
|
||||
// draft standard at the time of PSO's development, but it wasn't widely used until over a decade later. IPv6 support
|
||||
@@ -861,7 +860,7 @@ struct S_GallonPlanResult_BB_25 {
|
||||
// 2D: Invalid command
|
||||
// 2E: Invalid command
|
||||
// 2F: Invalid command
|
||||
// 30: Invalid command
|
||||
// 30: Invalid command (but used as a newserv extension; see end of this file)
|
||||
// 31: Invalid command
|
||||
// 32: Invalid command
|
||||
// 33: Invalid command
|
||||
@@ -1234,12 +1233,12 @@ struct S_JoinGame_BB_64 : S_JoinGameT_DC_PC<PlayerLobbyDataBB> {
|
||||
// Similarly to 64, the client will ignore 64 and 65 commands while loading, and will buffer all other commands except
|
||||
// 1D until loading is done.
|
||||
|
||||
struct LobbyFlags_DCNTE {
|
||||
struct LobbyFlagsDCNTE {
|
||||
uint8_t client_id = 0;
|
||||
uint8_t leader_id = 0;
|
||||
uint8_t disable_udp = 1;
|
||||
uint8_t unused = 0;
|
||||
} __packed_ws__(LobbyFlags_DCNTE, 4);
|
||||
} __packed_ws__(LobbyFlagsDCNTE, 4);
|
||||
|
||||
struct LobbyFlags {
|
||||
uint8_t client_id = 0;
|
||||
@@ -1273,7 +1272,7 @@ struct S_JoinLobbyT {
|
||||
return offsetof(S_JoinLobbyT, entries) + used_entries * sizeof(Entry);
|
||||
}
|
||||
} __attribute__((packed));
|
||||
using S_JoinLobby_DCNTE_65_67_68 = S_JoinLobbyT<LobbyFlags_DCNTE, PlayerLobbyDataDCGC, PlayerDispDataDCPCV3>;
|
||||
using S_JoinLobby_DCNTE_65_67_68 = S_JoinLobbyT<LobbyFlagsDCNTE, PlayerLobbyDataDCGC, PlayerDispDataDCPCV3>;
|
||||
using S_JoinLobby_PC_65_67_68 = S_JoinLobbyT<LobbyFlags, PlayerLobbyDataPC, PlayerDispDataDCPCV3>;
|
||||
using S_JoinLobby_DC_GC_65_67_68_Ep3_EB = S_JoinLobbyT<LobbyFlags, PlayerLobbyDataDCGC, PlayerDispDataDCPCV3>;
|
||||
using S_JoinLobby_BB_65_67_68 = S_JoinLobbyT<LobbyFlags, PlayerLobbyDataBB, PlayerDispDataBB>;
|
||||
@@ -1523,7 +1522,7 @@ struct C_ConnectionInfo_DCNTE_8A {
|
||||
// client responds with an 8B command.
|
||||
|
||||
// 8A (C->S): Request lobby/game name (except DC NTE)
|
||||
// No arguments
|
||||
// No arguments.
|
||||
|
||||
// 8A (S->C): Lobby/game name (except DC NTE)
|
||||
// Contents is a string containing the lobby or game name. All versions after DCv1 send an 8A command to request the
|
||||
@@ -1576,7 +1575,7 @@ struct C_LoginV1_DC_PC_V3_90 {
|
||||
// despite its size not being a multiple of 4. This is fixed in later versions, so we have to handle both cases.
|
||||
} __packed_ws__(C_LoginV1_DC_PC_V3_90, 0x22);
|
||||
|
||||
// 90 (S->C): Account verification result (V3)
|
||||
// 90 (S->C): Account verification result (DC/PC/V3)
|
||||
// Behaves exactly the same as 9A (S->C). No arguments except header.flag.
|
||||
|
||||
// 91 (S->C): Start encryption at login server (legacy; non-BB only)
|
||||
@@ -1604,7 +1603,7 @@ struct C_RegisterV1_DC_92 {
|
||||
// Internal name: RcvPsoRegist
|
||||
// Same format and usage as 9C (S->C) command.
|
||||
|
||||
// 93 (C->S): Log in (DCv1)
|
||||
// 93 (C->S): Log in (DCv1, BB)
|
||||
|
||||
struct C_LoginV1_DC_93 {
|
||||
/* 00 */ le_uint32_t player_tag = 0x00010000;
|
||||
@@ -1627,8 +1626,6 @@ struct C_LoginExtendedV1_DC_93 : C_LoginV1_DC_93 {
|
||||
SC_MeetUserExtension_DC_V3 extension;
|
||||
} __packed_ws__(C_LoginExtendedV1_DC_93, 0x110);
|
||||
|
||||
// 93 (C->S): Log in (BB)
|
||||
|
||||
struct C_LoginBase_BB_93 {
|
||||
/* 00 */ le_uint32_t player_tag = 0x00010000;
|
||||
/* 04 */ le_uint32_t guild_card_number = 0;
|
||||
@@ -1674,7 +1671,7 @@ struct C_LoginWithHardwareInfo_BB_93 : C_LoginBase_BB_93 {
|
||||
|
||||
// 95 (S->C): Request player data
|
||||
// Internal name: RcvRecognition
|
||||
// No arguments
|
||||
// No arguments.
|
||||
// For some reason, some servers send high values in the header.flag field here. The header.flag field is completely
|
||||
// unused by the client, however - sending zero works just fine. The original Sega servers had some uninitialized
|
||||
// memory bugs, of which that may have been one, and other private servers may have just duplicated Sega's behavior.
|
||||
@@ -1698,7 +1695,7 @@ struct C_CharSaveInfo_DCv2_PC_V3_BB_96 {
|
||||
|
||||
// 97 (S->C): Save to memory card
|
||||
// Internal name: RcvSaveCountCheck
|
||||
// No arguments
|
||||
// No arguments.
|
||||
// Internally, this command is called RcvSaveCountCheck, even though the counter in the 96 command (to which 97 is a
|
||||
// reply) counts more events than saves. Sending this command with header.flag == 0 will show a message saying that
|
||||
// "character data was improperly saved", and will delete the character's items and challenge mode records. newserv
|
||||
@@ -1718,7 +1715,7 @@ struct C_CharSaveInfo_DCv2_PC_V3_BB_96 {
|
||||
|
||||
// 99 (C->S): Server time accepted
|
||||
// Internal name: SndPsoDirList
|
||||
// No arguments
|
||||
// No arguments.
|
||||
// This command's internal name suggests that it's actually a request for the ship select menu, but it's only sent as
|
||||
// the response to a B1 command (server time) and the client doesn't set any state to indicate it's waiting for a ship
|
||||
// select menu, so we just treat it as confirmation of a received B1 command instead.
|
||||
@@ -1763,14 +1760,13 @@ struct C_Login_DC_PC_V3_9A {
|
||||
// 13 = servers under maintenance (118)
|
||||
// Seems like most (all?) of the rest of the codes are "network error" (119).
|
||||
|
||||
// 9B (S->C): Secondary server init (non-BB, non-DCv1)
|
||||
// Behaves exactly the same as 17 (S->C).
|
||||
|
||||
// 9B (S->C): Secondary server init (BB)
|
||||
// Format is the same as 03 (and the client uses the same encryption afterward). The only differences:
|
||||
// 9B (S->C): Secondary server init (DCv2 and later)
|
||||
// On versions before BB, this command behaves exactly the same as 17 (S->C).
|
||||
// On BB, the format of this command is the same as 03 (and the client uses the same encryption afterward). The only
|
||||
// differences are:
|
||||
// - 9B does not work during the data-server phase (before the client has reached the ship select menu), but 03 does.
|
||||
// - For command 9B, the copyright string must be "PSO NEW PM Server. Copyright 1999-2002 SONICTEAM.".
|
||||
// - The client will respond to 9B with DB instead of 93.
|
||||
// - The client will respond to this command with a DB command instead of a 93 command.
|
||||
|
||||
// 9C (C->S): Register
|
||||
// Internal name: SndPsoRegist
|
||||
@@ -1826,13 +1822,13 @@ struct C_Login_DC_PC_GC_9D {
|
||||
/* 08 */ be_uint64_t hardware_id;
|
||||
/* 10 */ le_uint32_t sub_version = 0;
|
||||
/* 14 */ uint8_t is_extended = 0; // If 1, structure has extended format
|
||||
/* 15 */ Language language = Language::JAPANESE; // 0 = JP, 1 = EN, 2 = DE, 3 = FR, 4 = ES
|
||||
/* 15 */ Language language = Language::JAPANESE;
|
||||
/* 16 */ parray<uint8_t, 0x2> unused3; // Always zeroes
|
||||
/* 18 */ pstring<TextEncoding::ASCII, 0x10> v1_serial_number;
|
||||
/* 28 */ pstring<TextEncoding::ASCII, 0x10> v1_access_key;
|
||||
/* 38 */ pstring<TextEncoding::ASCII, 0x10> serial_number; // On XB, this is the XBL gamertag
|
||||
/* 48 */ pstring<TextEncoding::ASCII, 0x10> access_key; // On XB, this is the XBL user ID
|
||||
/* 58 */ pstring<TextEncoding::ASCII, 0x30> serial_number2; // On DCv2, this is the hardware ID; on XB, this is the XBL gamertag
|
||||
/* 58 */ pstring<TextEncoding::ASCII, 0x30> serial_number2; // DCv2: hardware ID; XB: XBL gamertag
|
||||
/* 88 */ pstring<TextEncoding::ASCII, 0x30> access_key2; // On XB, this is the XBL user ID
|
||||
/* B8 */ pstring<TextEncoding::ASCII, 0x10> login_character_name;
|
||||
/* C8 */
|
||||
@@ -1936,7 +1932,7 @@ struct C_ChangeShipOrBlock_A0_A1 {
|
||||
// Same as 07 command.
|
||||
|
||||
// A2 (C->S): Request quest menu
|
||||
// No arguments
|
||||
// No arguments.
|
||||
|
||||
// A2 (S->C): Quest menu
|
||||
// Client will respond with an 09, 10, or A9 command. For 09, the server should send the category or quest description
|
||||
@@ -2014,7 +2010,7 @@ struct S_QuestMenuEntry_BB_A2_A4 {
|
||||
|
||||
// A9 (C->S): Quest menu closed (canceled)
|
||||
// Internal name: SndQuestEnd
|
||||
// No arguments
|
||||
// No arguments.
|
||||
// This command is sent when the in-game quest menu (A2) is closed. This is used by the server to unlock the game if
|
||||
// the players don't select a quest, since players are forbidden from joining while the quest menu is open. When the
|
||||
// download quest menu is closed, either by downloading a quest or canceling, the client sends A0 instead.
|
||||
@@ -2148,7 +2144,7 @@ struct S_RankUpdate_Ep3_B7 {
|
||||
} __packed_ws__(S_RankUpdate_Ep3_B7, 0x1C);
|
||||
|
||||
// B7 (C->S): Confirm rank update (Episode 3)
|
||||
// No arguments
|
||||
// No arguments.
|
||||
// The client sends this after it receives a B7 from the server.
|
||||
|
||||
// B8 (S->C): Update card definitions (Episode 3)
|
||||
@@ -2159,7 +2155,7 @@ struct S_RankUpdate_Ep3_B7 {
|
||||
// Note: PSO BB accepts this command as well, but ignores it.
|
||||
|
||||
// B8 (C->S): Confirm updated card definitions (Episode 3)
|
||||
// No arguments
|
||||
// No arguments.
|
||||
// The client sends this after it receives a B8 from the server.
|
||||
|
||||
// B8 (C->S): Valid but ignored (BB)
|
||||
@@ -2457,21 +2453,33 @@ struct S_ConfirmTournamentEntry_Ep3_CC {
|
||||
// CF: Invalid command
|
||||
|
||||
// D0 (C->S): Start trade sequence (V3/BB)
|
||||
// The trade window sequence is a bit complicated. On pre-BB versions, the normal flow is:
|
||||
// The trade window sequence is a bit complicated. On pre-BB versions, the sequence is:
|
||||
// - Clients sync trade state with 6xA6 commands
|
||||
// - When both have confirmed, one client (the initiator) sends D0
|
||||
// - The server sends D1 to the other client (the responder)
|
||||
// - The responder sends D0
|
||||
// - When both have confirmed, one client (the initiator) sends D0 with the items it intends to give to the other
|
||||
// player (the responder)
|
||||
// - The server sends D1 to the responder
|
||||
// - The responder sends D0 with its own item list
|
||||
// - The server sends D1 to both clients
|
||||
// - Both clients delete the sent items from their inventories and send the appropriate subcommand (6x29)
|
||||
// - Both clients delete the sent items from their inventories and send the appropriate subcommands (6x29)
|
||||
// - Both clients send D2; similarly to how AC works, the server doesn't proceed until both D2 commands are received
|
||||
// - The server sends D3 to both clients with each other's data from their D0 commands, followed immediately by D4 01
|
||||
// to both clients, which completes the trade
|
||||
// - Both clients send the appropriate subcommand to create inventory items
|
||||
// On BB, the flow is similar, except after both D2 commands are received, the server instead handles the rest of the
|
||||
// process - it sends 6x29 commands to delete the inventory items and 6xBE to create the traded items.
|
||||
// At any point if an error occurs, either client may send a D4 00, which cancels the entire sequence. The server
|
||||
// should then send D4 00 to both clients.
|
||||
// - Both clients send the appropriate subcommands to create inventory items (6x5E)
|
||||
// On BB, the sequence is:
|
||||
// - Clients sync trade state with 6xA6 commands
|
||||
// - When both have confirmed, one client (the initiator) sends D0 with the items it intends to give to the other
|
||||
// player (the responder)
|
||||
// - The server sends D1 to the responder
|
||||
// - The responder sends D0 with its own item list
|
||||
// - The server sends D1 to both clients
|
||||
// - Both clients send D2; similarly to how AC works, the server doesn't proceed until both D2 commands are received
|
||||
// - The server executes the trade by sending 6x29 for each item given away from each player, followed by 6xBE to
|
||||
// create each item in the other player's inventory (note that all 6x29 commands must be sent before any 6xBE
|
||||
// command, since the client's item state may desync if one player's inventory is full and they receive a 6xBE)
|
||||
// - The server sends D3 followed immediately by D4 01 to both clients, which completes the trade; unlike in the pre-BB
|
||||
// case, the D3 commands are empty (no item list is sent)
|
||||
// At any point if an error occurs, either client may send D4 00, which cancels the entire sequence. The server should
|
||||
// then send D4 00 to both clients.
|
||||
|
||||
struct SC_TradeItems_D0_D3 { // D0 when sent by client, D3 when sent by server
|
||||
le_uint16_t target_client_id = 0;
|
||||
@@ -2492,19 +2500,20 @@ struct SC_TradeItems_D0_D3 { // D0 when sent by client, D3 when sent by server
|
||||
// On BB, this command has no arguments (and the server generates the appropriate delete and create inventory item
|
||||
// commands), but the D3 command must still must be sent before the D4 command to advance the trade state.
|
||||
|
||||
// D4 (C->S): Trade failed (V3/BB)
|
||||
// D4 (C->S): Cancel/fail trade sequence (V3/BB)
|
||||
// No arguments. See the description of D0 for usage information.
|
||||
|
||||
// D4 (S->C): Trade complete (V3/BB)
|
||||
// header.flag must be 0 (trade failed) or 1 (trade complete). See the description of D0 for usage information.
|
||||
|
||||
// D5: Large message box (V3/BB)
|
||||
// D5 (S->C): Large message box (V3/BB)
|
||||
// Same as 1A command, except the maximum length of the message is 0x1000 bytes. On BB, this command is not valid
|
||||
// during the data server phase (whereas 1A is valid there). The BB client ignores all D5 commands after the first one
|
||||
// sent in each connection; this logic does not apply to 1A.
|
||||
// sent in each connection; this logic does not apply to 1A. This logic was added for some reason in BB; V3 does not
|
||||
// have this restriction.
|
||||
|
||||
// D6 (C->S): Large message box closed (V3)
|
||||
// No arguments
|
||||
// No arguments.
|
||||
// DC, PC, and BB do not send this command at all. GC US v1.0 and v1.1 will send this command when any large message
|
||||
// box (1A/D5) is closed; GC Plus and Episode 3 will send D6 only for large message boxes that occur before the client
|
||||
// has joined a lobby. (After joining a lobby, large message boxes will still be displayed if sent by the server, but
|
||||
@@ -2523,7 +2532,7 @@ struct C_GBAGameRequest_V3_D7 {
|
||||
} __packed_ws__(C_GBAGameRequest_V3_D7, 0x10);
|
||||
|
||||
// D7 (S->C): GBA file not found (V3/BB)
|
||||
// No arguments
|
||||
// No arguments.
|
||||
// This command is not valid on PSO GC Episodes 1&2 Trial Edition. PSO BB accepts but completely ignores this command.
|
||||
// This command tells the client that the file it requested via a D7 command does not exist. This causes the F8C1
|
||||
// (get_dl_status) quest opcode to return 0 (file not found), rather than 1 (download in progress) or 2 (complete).
|
||||
@@ -2554,7 +2563,10 @@ check_struct_size(S_InfoBoardEntry_BB_D8, 0x178);
|
||||
// This command is not valid on PSO GC Episodes 1&2 Trial Edition.
|
||||
|
||||
// DB (C->S): Verify license (V3/BB)
|
||||
// Server should respond with a 9A command.
|
||||
// Server should respond with a 9A command. But Insignia's proxy never sends this command to the remote server, so
|
||||
// newserv will only see this command from an Xbox client if it's connected through a different Xbox Live
|
||||
// implementation. For this reason, newserv does not check the credentials when it receives DB from an Xbox client; it
|
||||
// instead sends 9A and checks the credentials when the client subsequently sends 9E.
|
||||
|
||||
struct C_VerifyAccount_V3_DB {
|
||||
pstring<TextEncoding::ASCII, 0x10> v1_serial_number; // Unused
|
||||
@@ -3161,7 +3173,7 @@ struct SC_TeamChat_BB_07EA {
|
||||
} __packed_ws__(SC_TeamChat_BB_07EA, 0x20);
|
||||
|
||||
// 08EA (C->S): Get team member list
|
||||
// No arguments
|
||||
// No arguments.
|
||||
|
||||
// 09EA (S->C): Team member list
|
||||
|
||||
@@ -3188,7 +3200,7 @@ struct S_Unknown_BB_0CEA {
|
||||
} __packed_ws__(S_Unknown_BB_0CEA, 0x20);
|
||||
|
||||
// 0DEA (C->S): Get team name
|
||||
// No arguments
|
||||
// No arguments.
|
||||
|
||||
// 0EEA (S->C): Team name
|
||||
|
||||
@@ -3251,7 +3263,7 @@ struct S_TeamInfoForPlayer_BB_13EA_15EA_Entry {
|
||||
// No arguments except header.flag, which is zero if the transfer failed or nonzero if it succeeded.
|
||||
|
||||
// 18EA: Intra-team ranking information
|
||||
// No arguments (C->S)
|
||||
// No arguments (C->S).
|
||||
|
||||
struct S_IntraTeamRanking_BB_18EA {
|
||||
/* 0000 */ le_uint32_t ranking_points = 0;
|
||||
@@ -3271,7 +3283,7 @@ struct S_IntraTeamRanking_BB_18EA {
|
||||
} __packed_ws__(S_IntraTeamRanking_BB_18EA, 0x10);
|
||||
|
||||
// 19EA: Team reward list
|
||||
// No arguments (C->S)
|
||||
// No arguments (C->S).
|
||||
|
||||
struct S_TeamRewardList_BB_19EA_1AEA {
|
||||
le_uint32_t num_entries;
|
||||
@@ -3434,7 +3446,7 @@ struct S_AdvanceCardTradeState_Ep3_EE_FlagD1 {
|
||||
} __packed_ws__(S_AdvanceCardTradeState_Ep3_EE_FlagD1, 4);
|
||||
|
||||
// EE D2 (C->S): Trade can proceed
|
||||
// No arguments
|
||||
// No arguments.
|
||||
|
||||
// EE D3 (S->C): Execute trade
|
||||
// Same format as EE D0
|
||||
@@ -3710,12 +3722,53 @@ struct G_UpdateEnemyStateT_6x0A {
|
||||
G_EntityIDHeader header;
|
||||
le_uint16_t enemy_index = 0; // [0, 0xB50)
|
||||
le_uint16_t total_damage = 0;
|
||||
typename std::conditional_t<BE, be_uint32_t, le_uint32_t> game_flags = 0;
|
||||
} __attribute__((packed));
|
||||
// The bits in game_flags mean (all addresses in TODOs are for 3OE1):
|
||||
// 00000001 = is poisoned
|
||||
// 00000002 = is paralyzed
|
||||
// 00000004 = is shocked
|
||||
// 00000008 = is slow
|
||||
// 00000010 = is confused
|
||||
// 00000020 = is frozen
|
||||
// 00000040 = cures and prevents all negative status effects
|
||||
// 00000080 = appears to be unused (TODO: look for any usage of this flag)
|
||||
// 00000100 = missed by attack (often set immediately before showing red "MISS" text)
|
||||
// 00000200 = hit by attack (causes flinch for most enemies)
|
||||
// 00000400 = last hit did damage greater than 25% of enemy's max HP (some enemies don't clear this)
|
||||
// 00000800 = is dead (when set for most enemies, plays the death animation and then destroys the enemy)
|
||||
// 00001000 = unknown (TODO: see TObjEnemyV8048ee80_v1C, TObjEnemyV8048ee80_v3B, TObjEneDolmOlm_v3B)
|
||||
// 00002000 = unknown (TODO: see TObjGrass_v1E, 8011EA08, TObjEneIllGill_v1E, TObjEneIllGill_init)
|
||||
// 00004000 = unknown (TODO: has status effect in slot 5; De Rol Le uses this; Vol Opt uses it too at
|
||||
// TBoss3Volopt_update, TBoss3VoloptCore_update, TBoss3VoloptP01_update, TBoss3VolOptP02_update,
|
||||
// TObjectV8047c128_v39, TObjectV8047c128_v38; related to paralysis somehow? see
|
||||
// TObjectV8047c128_v24_update_paralysis_effect)
|
||||
// 00008000 = immune to freeze (TODO: see TBoss3VolOptP02_init_inner; maybe other things use it too)
|
||||
// 00010000 = unknown (TODO: see 801BA1F8)
|
||||
// 00020000 = can't attack, cast techs, or use items (e.g. Vol Opt cage and Ruins falling traps set this)
|
||||
// 00040000 = untargetable (e.g. TObjEneBeast sets this at construction time; it's cleared when it roars)
|
||||
// 00080000 = appears to be unused (TODO: look for any usage of this flag)
|
||||
// 00100000 = for players, is near enemy; for some enemies, is activated (not set if in idle animations, e.g. for
|
||||
// TObjEneBeast and related classes) (unverified on v2)
|
||||
// 00200000 = is attacking? (TODO: also set when TObjEneMoja is jumping though; also see TObjEnemyV8048ee80_v3C,
|
||||
// TObjEneMerillLia_v3C, TObjEneSaver_v3A)
|
||||
// 00400000 = unknown (TODO: see TOSensor_vF, 801CC358, 801CD224)
|
||||
// 00800000 = affected by gravity? can be aerial? (wolves don't have this, perhaps their jumps are hardcoded; see
|
||||
// TObjEnemyV8048ee80_v5B) (unverified on v2)
|
||||
// 01000000 = immune to shock and freeze and paralysis (for Canadine/Mothmant/etc, only set when they're higher
|
||||
// than player level; TODO: maybe other things too; also used by TObjPlayer, see 801B84B4; also has a
|
||||
// meaning for weapons, see TItemWeapon_v10) (unverified on v2)
|
||||
// 02000000 = invisible (also suppresses particles, some sounds, and hit/miss text)
|
||||
// 04000000 = temporarily invincible (e.g. Dragon while it roars to advance to phase 2) (unverified on v2)
|
||||
// 08000000 = unknown (TODO; see 80113384, TItemMag_v1B, TItemMag_v1A, 801182B0, TObjPlayer_render; probably
|
||||
// graphical effects only)
|
||||
// 10000000 = entity is player
|
||||
// 20000000 = entity is enemy
|
||||
// 40000000 = entity is object (some entities have both this and 20000000 set; this appears to make TWindowLockOn
|
||||
// not show anything but the entity is still attackable, see TWindowLockOn_should_show_for_entity)
|
||||
// 80000000 = entity is item
|
||||
U32T<BE> game_flags = 0;
|
||||
} __packed_ws_be__(G_UpdateEnemyStateT_6x0A, 0x0C);
|
||||
using G_UpdateEnemyState_GC_6x0A = G_UpdateEnemyStateT_6x0A<true>;
|
||||
using G_UpdateEnemyState_DC_PC_XB_BB_6x0A = G_UpdateEnemyStateT_6x0A<false>;
|
||||
check_struct_size(G_UpdateEnemyState_GC_6x0A, 0x0C);
|
||||
check_struct_size(G_UpdateEnemyState_DC_PC_XB_BB_6x0A, 0x0C);
|
||||
|
||||
// 6x0B: Update object state
|
||||
|
||||
@@ -3726,13 +3779,14 @@ struct G_UpdateObjectState_6x0B {
|
||||
} __packed_ws__(G_UpdateObjectState_6x0B, 0x0C);
|
||||
|
||||
// 6x0C: Add status effect (poison/slow/etc.) (protected on V3/V4)
|
||||
// 6x0D: Remove status effect (protected on V3/V4)
|
||||
|
||||
struct G_AddStatusEffect_6x0C {
|
||||
struct G_AddOrRemoveStatusEffect_6x0C_6x0D {
|
||||
G_ClientIDHeader header;
|
||||
// Each status effect has an assigned slot; there are 5 slots and each slot may only hold one effect at a time. (The
|
||||
// last slot, slot 4, is unused.) If a new status effect is added to a slot that already contains one, the existing
|
||||
// status effect is replaced. Non-technique status effects have fixed or indefinite durations; technique-based
|
||||
// effects have durations based on the technique's level.
|
||||
// last slot, slot 4, may be used by certain boss effects; this is unconfirmed.) If a new status effect is added to a
|
||||
// slot that already contains one, the existing status effect is replaced. Non-technique status effects have fixed or
|
||||
// indefinite durations; technique-based effects have durations based on the technique's level.
|
||||
// Values for effect_type:
|
||||
// 02 = Freeze (slot 1; 5 seconds)
|
||||
// 03 = Shock (slot 1; 10 seconds)
|
||||
@@ -3747,16 +3801,8 @@ struct G_AddStatusEffect_6x0C {
|
||||
// 12 = Confuse (slot 1; 10 seconds)
|
||||
// Anything else = command is ignored
|
||||
le_uint32_t effect_type = 0;
|
||||
le_float amount = 0; // Only used for Shifta/Deband/Jellen/Zalure
|
||||
} __packed_ws__(G_AddStatusEffect_6x0C, 0x0C);
|
||||
|
||||
// 6x0D: Clear status effect slot (protected on V3/V4)
|
||||
|
||||
struct G_RemoveStatusEffect_6x0D {
|
||||
G_ClientIDHeader header;
|
||||
le_uint32_t slot = 0; // See 6x0C description for slot values
|
||||
le_uint32_t unused = 0;
|
||||
} __packed_ws__(G_RemoveStatusEffect_6x0D, 0x0C);
|
||||
le_float amount = 0; // Only used in 6x0C for Shifta/Deband/Jellen/Zalure; unused in 6x0D
|
||||
} __packed_ws__(G_AddOrRemoveStatusEffect_6x0C_6x0D, 0x0C);
|
||||
|
||||
// 6x0E: Clear all negative status effects (protected on V3/V4)
|
||||
// It seems that the client never sends this command.
|
||||
@@ -3788,11 +3834,9 @@ struct G_DragonBossActionsT_6x12 {
|
||||
le_uint32_t target_client_id = 0xFFFF; // 0xFFFF (not 0xFFFFFFFF) means no target
|
||||
F32T<BE> x = 0.0f;
|
||||
F32T<BE> z = 0.0f;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws_be__(G_DragonBossActionsT_6x12, 0x14);
|
||||
using G_DragonBossActions_DC_PC_XB_BB_6x12 = G_DragonBossActionsT_6x12<false>;
|
||||
using G_DragonBossActions_GC_6x12 = G_DragonBossActionsT_6x12<true>;
|
||||
check_struct_size(G_DragonBossActions_DC_PC_XB_BB_6x12, 0x14);
|
||||
check_struct_size(G_DragonBossActions_GC_6x12, 0x14);
|
||||
|
||||
// 6x13: De Rol Le boss actions (not valid on Episode 3)
|
||||
|
||||
@@ -3873,7 +3917,7 @@ struct G_DisablePKModeForPlayer_6x1C {
|
||||
// 6x1D: Request partial player data (pre-v1 only)
|
||||
// The subcommand number 6x1D is not used in any final version of PSO; this number is assigned based on what the
|
||||
// command number would be if it were. On DC NTE, this is subcommand 6x19; on 11/2000, it's 6x1B. This command does not
|
||||
// appear to ever be sent by the client; however, it will respond with 6x1E if it receives this command.
|
||||
// appear to ever be sent by the client; however, it will respond with a 6x1E command if it receives this command.
|
||||
|
||||
struct G_RequestPartialPlayerData_DCProtos_6x1D {
|
||||
G_UnusedHeader header;
|
||||
@@ -4045,8 +4089,8 @@ struct G_ChangePlayerHP_6x2F {
|
||||
} __packed_ws__(G_ChangePlayerHP_6x2F, 0x0C);
|
||||
|
||||
// 6x30: Change player level (protected on GC NTE/V3 but not V4)
|
||||
// On DC NTE, the updated stats aren't sent, and the client may only gain a single level at once. On other versions,
|
||||
// this is not the case.
|
||||
// On DC NTE and 11/2000, the updated stats aren't sent, and the client may only gain a single level at once. On other
|
||||
// versions, this is not the case.
|
||||
|
||||
struct G_ChangePlayerLevel_DCNTE_6x30 {
|
||||
G_ClientIDHeader header;
|
||||
@@ -4112,7 +4156,7 @@ struct G_Unknown_6x36 {
|
||||
|
||||
struct G_PhotonBlast_6x37 {
|
||||
G_ClientIDHeader header;
|
||||
le_uint16_t unknown_a1 = 0;
|
||||
le_uint16_t amount = 0; // Amount of PB energy to expend (ignored by client upon receipt)
|
||||
le_uint16_t unused = 0;
|
||||
} __packed_ws__(G_PhotonBlast_6x37, 8);
|
||||
|
||||
@@ -4199,7 +4243,9 @@ struct G_SetPosition_6x3F {
|
||||
struct G_WalkToPosition_6x40 {
|
||||
G_ClientIDHeader header;
|
||||
VectorXZF pos;
|
||||
le_uint32_t action = 0;
|
||||
// In the flags field, bit 00000008 is set if game_flag 00100000 is set (same as is_near_enemy in 6x3E and 6x3F). The
|
||||
// meanings of the other bits are unknown.
|
||||
le_uint32_t flags = 0;
|
||||
} __packed_ws__(G_WalkToPosition_6x40, 0x10);
|
||||
|
||||
// 6x41: Move to position (v1)
|
||||
@@ -4277,8 +4323,8 @@ struct G_ShieldAttack_6x4A {
|
||||
G_ClientIDHeader header;
|
||||
} __packed_ws__(G_ShieldAttack_6x4A, 4);
|
||||
|
||||
// 6x4B: Hit by enemy (protected on GC NTE/V3/V4)
|
||||
// 6x4C: Hit by enemy (protected on GC NTE/V3/V4)
|
||||
// 6x4B: Minor hit by enemy (<= 25% of max HP; protected on GC NTE/V3/V4)
|
||||
// 6x4C: Major hit by enemy (> 25% of max HP; protected on GC NTE/V3/V4)
|
||||
|
||||
struct G_HitByEnemy_6x4B_6x4C {
|
||||
G_ClientIDHeader header;
|
||||
@@ -4380,7 +4426,7 @@ struct G_Unknown_6x57 {
|
||||
struct G_LobbyAnimation_6x58 {
|
||||
G_ClientIDHeader header;
|
||||
le_uint16_t animation_number = 0;
|
||||
le_uint16_t unused = 0;
|
||||
le_uint16_t flags = 0; // 0 = normal, 1 = use opposite gender's animation
|
||||
} __packed_ws__(G_LobbyAnimation_6x58, 8);
|
||||
|
||||
// 6x59: Pick up item
|
||||
@@ -4675,7 +4721,7 @@ struct G_6x70_Sub_Telepipe {
|
||||
struct G_6x70_Base_DCNTE {
|
||||
/* 0000 */ le_uint16_t client_id = 0;
|
||||
/* 0002 */ le_uint16_t room_id = 0;
|
||||
/* 0004 */ le_uint32_t flags1 = 0;
|
||||
/* 0004 */ le_uint32_t game_flags = 0;
|
||||
/* 0008 */ VectorXYZF pos;
|
||||
/* 0014 */ VectorXYZI angle;
|
||||
/* 0020 */ le_uint16_t phase = 0;
|
||||
@@ -4683,41 +4729,39 @@ struct G_6x70_Base_DCNTE {
|
||||
} __packed_ws__(G_6x70_Base_DCNTE, 0x24);
|
||||
|
||||
struct G_SyncPlayerDispAndInventory_DCNTE_6x70 {
|
||||
// Offsets in this struct are relative to the overall command header
|
||||
/* 0004 */ G_ExtendedHeaderT<G_ClientIDHeader> header = {{0x60, 0x00, 0x0000}, sizeof(G_SyncPlayerDispAndInventory_DCNTE_6x70)};
|
||||
/* 000C */ G_6x70_Base_DCNTE base;
|
||||
/* 0000 */ G_ExtendedHeaderT<G_ClientIDHeader> header = {{0x60, 0x00, 0x0000}, sizeof(G_SyncPlayerDispAndInventory_DCNTE_6x70)};
|
||||
/* 0008 */ G_6x70_Base_DCNTE base;
|
||||
// The following two fields appear to contain uninitialized data
|
||||
/* 0030 */ le_uint32_t unknown_a5 = 0;
|
||||
/* 0034 */ le_uint32_t unknown_a6 = 0;
|
||||
/* 0038 */ G_6x70_Sub_Telepipe telepipe;
|
||||
/* 0054 */ le_uint32_t death_flags = 0;
|
||||
/* 0058 */ PlayerHoldState_DCProtos hold_state;
|
||||
/* 0068 */ le_uint32_t area = 0;
|
||||
/* 006C */ le_uint32_t game_flags = 0;
|
||||
/* 0070 */ PlayerVisualConfig visual;
|
||||
/* 00C0 */ PlayerStats stats;
|
||||
/* 00E4 */ le_uint32_t num_items = 0;
|
||||
/* 00E8 */ parray<PlayerInventoryItem, 0x1E> items;
|
||||
/* 0430 */
|
||||
/* 002C */ le_uint32_t unknown_a5 = 0;
|
||||
/* 0030 */ le_uint32_t unknown_a6 = 0;
|
||||
/* 0034 */ G_6x70_Sub_Telepipe telepipe;
|
||||
/* 0050 */ le_uint32_t death_flags = 0;
|
||||
/* 0054 */ PlayerHoldState_DCProtos hold_state;
|
||||
/* 0064 */ le_uint32_t area = 0;
|
||||
/* 0068 */ le_uint32_t player_flags = 0;
|
||||
/* 006C */ PlayerVisualConfig visual;
|
||||
/* 00BC */ PlayerStats stats;
|
||||
/* 00E0 */ le_uint32_t num_items = 0;
|
||||
/* 00E4 */ parray<PlayerInventoryItem, 0x1E> items;
|
||||
/* 042C */
|
||||
} __packed_ws__(G_SyncPlayerDispAndInventory_DCNTE_6x70, 0x42C);
|
||||
|
||||
struct G_SyncPlayerDispAndInventory_DC112000_6x70 {
|
||||
// Offsets in this struct are relative to the overall command header
|
||||
/* 0004 */ G_ExtendedHeaderT<G_ClientIDHeader> header = {{0x67, 0x00, 0x0000}, sizeof(G_SyncPlayerDispAndInventory_DC112000_6x70)};
|
||||
/* 000C */ G_6x70_Base_DCNTE base;
|
||||
/* 0030 */ le_uint16_t bonus_hp_from_materials = 0;
|
||||
/* 0032 */ le_uint16_t bonus_tp_from_materials = 0;
|
||||
/* 0034 */ parray<uint8_t, 0x10> unknown_a5;
|
||||
/* 0044 */ G_6x70_Sub_Telepipe telepipe;
|
||||
/* 0060 */ le_uint32_t death_flags = 0;
|
||||
/* 0064 */ PlayerHoldState_DCProtos hold_state;
|
||||
/* 0074 */ le_uint32_t area = 0;
|
||||
/* 0078 */ le_uint32_t game_flags = 0;
|
||||
/* 007C */ PlayerVisualConfig visual;
|
||||
/* 00CC */ PlayerStats stats;
|
||||
/* 00F0 */ le_uint32_t num_items = 0;
|
||||
/* 00F4 */ parray<PlayerInventoryItem, 0x1E> items;
|
||||
/* 043C */
|
||||
/* 0000 */ G_ExtendedHeaderT<G_ClientIDHeader> header = {{0x67, 0x00, 0x0000}, sizeof(G_SyncPlayerDispAndInventory_DC112000_6x70)};
|
||||
/* 0008 */ G_6x70_Base_DCNTE base;
|
||||
/* 002C */ le_uint16_t bonus_hp_from_materials = 0;
|
||||
/* 002E */ le_uint16_t bonus_tp_from_materials = 0;
|
||||
/* 0030 */ parray<uint8_t, 0x10> unknown_a5;
|
||||
/* 0040 */ G_6x70_Sub_Telepipe telepipe;
|
||||
/* 005C */ le_uint32_t death_flags = 0;
|
||||
/* 0060 */ PlayerHoldState_DCProtos hold_state;
|
||||
/* 0070 */ le_uint32_t area = 0;
|
||||
/* 0074 */ le_uint32_t player_flags = 0;
|
||||
/* 0078 */ PlayerVisualConfig visual;
|
||||
/* 00C8 */ PlayerStats stats;
|
||||
/* 00EC */ le_uint32_t num_items = 0;
|
||||
/* 00F0 */ parray<PlayerInventoryItem, 0x1E> items;
|
||||
/* 0438 */
|
||||
} __packed_ws__(G_SyncPlayerDispAndInventory_DC112000_6x70, 0x438);
|
||||
|
||||
struct G_6x70_Base_V1 {
|
||||
@@ -4728,71 +4772,71 @@ struct G_6x70_Base_V1 {
|
||||
/* 0034 */ StatusEffectState temporary_status_effect;
|
||||
/* 0040 */ StatusEffectState attack_status_effect;
|
||||
/* 004C */ StatusEffectState defense_status_effect;
|
||||
/* 0058 */ StatusEffectState unused_status_effect;
|
||||
/* 0058 */ StatusEffectState unknown_a1_status_effect;
|
||||
/* 0064 */ le_uint32_t language32 = 0;
|
||||
/* 0068 */ le_uint32_t player_tag = 0;
|
||||
/* 006C */ le_uint32_t guild_card_number = 0;
|
||||
/* 0070 */ le_uint32_t unknown_a6 = 0; // Probably battle-related (assigned together with battle_team_number)
|
||||
/* 0074 */ le_uint32_t battle_team_number = 0;
|
||||
/* 0078 */ G_6x70_Sub_Telepipe telepipe;
|
||||
/* 0094 */ le_uint32_t death_flags = 0; // Only a few bits are used. 4 = player is dead
|
||||
// Only a few bits appear to be used in death_flags. Known values:
|
||||
// 00000001 = should drop weapon/item on death
|
||||
// 00000002 = does not have any automatic revival item (Scape Doll or Ragol Ring)
|
||||
// 00000004 = was revived by automatic revival item
|
||||
/* 0094 */ le_uint32_t death_flags = 0;
|
||||
/* 0098 */ PlayerHoldState hold_state;
|
||||
/* 00AC */ le_uint32_t area = 0;
|
||||
/* 00B0 */ le_uint32_t game_flags = 0;
|
||||
/* 00B0 */ le_uint32_t player_flags = 0;
|
||||
/* 00B4 */ parray<uint8_t, 0x14> technique_levels_v1 = 0xFF; // Last byte is uninitialized
|
||||
/* 00C8 */ PlayerVisualConfig visual;
|
||||
/* 0118 */
|
||||
} __packed_ws__(G_6x70_Base_V1, 0x118);
|
||||
|
||||
struct G_SyncPlayerDispAndInventory_DC_PC_6x70 {
|
||||
// Offsets in this struct are relative to the overall command header
|
||||
/* 0004 */ G_ExtendedHeaderT<G_ClientIDHeader> header = {{0x70, 0x00, 0x0000}, sizeof(G_SyncPlayerDispAndInventory_DC_PC_6x70)};
|
||||
/* 000C */ G_6x70_Base_V1 base;
|
||||
/* 0124 */ PlayerStats stats;
|
||||
/* 0148 */ le_uint32_t num_items = 0;
|
||||
/* 014C */ parray<PlayerInventoryItem, 0x1E> items;
|
||||
/* 0494 */
|
||||
/* 0000 */ G_ExtendedHeaderT<G_ClientIDHeader> header = {{0x70, 0x00, 0x0000}, sizeof(G_SyncPlayerDispAndInventory_DC_PC_6x70)};
|
||||
/* 0008 */ G_6x70_Base_V1 base;
|
||||
/* 0120 */ PlayerStats stats;
|
||||
/* 0144 */ le_uint32_t num_items = 0;
|
||||
/* 0148 */ parray<PlayerInventoryItem, 0x1E> items;
|
||||
/* 0490 */
|
||||
} __packed_ws__(G_SyncPlayerDispAndInventory_DC_PC_6x70, 0x490);
|
||||
|
||||
// GC NTE also uses this format.
|
||||
// GC NTE also uses this format
|
||||
struct G_SyncPlayerDispAndInventory_GC_6x70 {
|
||||
// Offsets in this struct are relative to the overall command header
|
||||
/* 0004 */ G_ExtendedHeaderT<G_ClientIDHeader> header = {{0x70, 0x00, 0x0000}, sizeof(G_SyncPlayerDispAndInventory_GC_6x70)};
|
||||
/* 000C */ G_6x70_Base_V1 base;
|
||||
/* 0124 */ PlayerStats stats;
|
||||
/* 0148 */ le_uint32_t num_items = 0;
|
||||
/* 014C */ parray<PlayerInventoryItem, 0x1E> items;
|
||||
/* 0494 */ le_uint32_t floor = 0;
|
||||
/* 0498 */
|
||||
/* 0000 */ G_ExtendedHeaderT<G_ClientIDHeader> header = {{0x70, 0x00, 0x0000}, sizeof(G_SyncPlayerDispAndInventory_GC_6x70)};
|
||||
/* 0008 */ G_6x70_Base_V1 base;
|
||||
/* 0120 */ PlayerStats stats;
|
||||
/* 0144 */ le_uint32_t num_items = 0;
|
||||
/* 0148 */ parray<PlayerInventoryItem, 0x1E> items;
|
||||
/* 0490 */ le_uint32_t floor = 0;
|
||||
/* 0494 */
|
||||
} __packed_ws__(G_SyncPlayerDispAndInventory_GC_6x70, 0x494);
|
||||
|
||||
struct G_SyncPlayerDispAndInventory_XB_6x70 {
|
||||
// Offsets in this struct are relative to the overall command header
|
||||
/* 0004 */ G_ExtendedHeaderT<G_ClientIDHeader> header = {{0x70, 0x00, 0x0000}, sizeof(G_SyncPlayerDispAndInventory_XB_6x70)};
|
||||
/* 000C */ G_6x70_Base_V1 base;
|
||||
/* 0124 */ PlayerStats stats;
|
||||
/* 0148 */ le_uint32_t num_items = 0;
|
||||
/* 014C */ parray<PlayerInventoryItem, 0x1E> items;
|
||||
/* 0494 */ le_uint32_t floor = 0;
|
||||
/* 0498 */ le_uint32_t xb_user_id_high = 0;
|
||||
/* 049C */ le_uint32_t xb_user_id_low = 0;
|
||||
/* 04A0 */ le_uint32_t unknown_a16 = 0;
|
||||
/* 04A4 */
|
||||
/* 0000 */ G_ExtendedHeaderT<G_ClientIDHeader> header = {{0x70, 0x00, 0x0000}, sizeof(G_SyncPlayerDispAndInventory_XB_6x70)};
|
||||
/* 0008 */ G_6x70_Base_V1 base;
|
||||
/* 0120 */ PlayerStats stats;
|
||||
/* 0144 */ le_uint32_t num_items = 0;
|
||||
/* 0148 */ parray<PlayerInventoryItem, 0x1E> items;
|
||||
/* 0490 */ le_uint32_t floor = 0;
|
||||
/* 0494 */ le_uint32_t xb_user_id_high = 0;
|
||||
/* 0498 */ le_uint32_t xb_user_id_low = 0;
|
||||
/* 049C */ le_uint32_t unknown_a16 = 0;
|
||||
/* 04A0 */
|
||||
} __packed_ws__(G_SyncPlayerDispAndInventory_XB_6x70, 0x4A0);
|
||||
|
||||
struct G_SyncPlayerDispAndInventory_BB_6x70 {
|
||||
// Offsets in this struct are relative to the overall command header
|
||||
/* 0008 */ G_ExtendedHeaderT<G_ClientIDHeader> header = {{0x70, 0x00, 0x0000}, sizeof(G_SyncPlayerDispAndInventory_BB_6x70)};
|
||||
/* 0010 */ G_6x70_Base_V1 base;
|
||||
/* 0128 */ pstring<TextEncoding::UTF16_ALWAYS_MARKED, 0x10> name;
|
||||
/* 0148 */ PlayerStats stats;
|
||||
/* 016C */ le_uint32_t num_items = 0;
|
||||
/* 0170 */ parray<PlayerInventoryItem, 0x1E> items;
|
||||
/* 04B8 */ le_uint32_t floor = 0;
|
||||
/* 04BC */ le_uint32_t xb_user_id_high = 0;
|
||||
/* 04C0 */ le_uint32_t xb_user_id_low = 0;
|
||||
/* 04C4 */ le_uint32_t unknown_a16 = 0;
|
||||
/* 04C8 */
|
||||
/* 0000 */ G_ExtendedHeaderT<G_ClientIDHeader> header = {{0x70, 0x00, 0x0000}, sizeof(G_SyncPlayerDispAndInventory_BB_6x70)};
|
||||
/* 0008 */ G_6x70_Base_V1 base;
|
||||
/* 0120 */ pstring<TextEncoding::UTF16_ALWAYS_MARKED, 0x10> name;
|
||||
/* 0140 */ PlayerStats stats;
|
||||
/* 0164 */ le_uint32_t num_items = 0;
|
||||
/* 0168 */ parray<PlayerInventoryItem, 0x1E> items;
|
||||
/* 04B0 */ le_uint32_t floor = 0;
|
||||
/* 04B4 */ le_uint32_t xb_user_id_high = 0;
|
||||
/* 04B8 */ le_uint32_t xb_user_id_low = 0;
|
||||
/* 04BC */ le_uint32_t unknown_a16 = 0;
|
||||
/* 04C0 */
|
||||
} __packed_ws__(G_SyncPlayerDispAndInventory_BB_6x70, 0x4C0);
|
||||
|
||||
// 6x71: Unblock game join (used while loading into game)
|
||||
@@ -4825,11 +4869,9 @@ struct G_WordSelectT_6x74 {
|
||||
uint8_t size = 0;
|
||||
U16T<BE> client_id = 0;
|
||||
WordSelectMessage message;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws_be__(G_WordSelectT_6x74, 0x20);
|
||||
using G_WordSelect_6x74 = G_WordSelectT_6x74<false>;
|
||||
using G_WordSelectBE_6x74 = G_WordSelectT_6x74<true>;
|
||||
check_struct_size(G_WordSelect_6x74, 0x20);
|
||||
check_struct_size(G_WordSelectBE_6x74, 0x20);
|
||||
|
||||
// 6x75: Update quest flag
|
||||
// This command does nothing on Episode 3.
|
||||
@@ -4957,11 +4999,9 @@ struct G_BattleScoresT_6x7F {
|
||||
} __packed_ws__(Entry, 8);
|
||||
G_UnusedHeader header;
|
||||
parray<Entry, 4> entries;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws_be__(G_BattleScoresT_6x7F, 0x24);
|
||||
using G_BattleScores_6x7F = G_BattleScoresT_6x7F<false>;
|
||||
using G_BattleScoresBE_6x7F = G_BattleScoresT_6x7F<true>;
|
||||
check_struct_size(G_BattleScores_6x7F, 0x24);
|
||||
check_struct_size(G_BattleScoresBE_6x7F, 0x24);
|
||||
|
||||
// 6x80: Trigger trap (not valid on Episode 3)
|
||||
|
||||
@@ -4994,6 +5034,8 @@ struct G_EnableDropWeaponOnDeath_6x82 {
|
||||
struct G_PlaceTrap_6x83 {
|
||||
G_ClientIDHeader header;
|
||||
le_uint16_t trap_type = 0;
|
||||
// trap_index is actually the number of traps remaining for this client after setting this one (so it counts backward
|
||||
// from their total trap count)
|
||||
le_uint16_t trap_index = 0;
|
||||
} __packed_ws__(G_PlaceTrap_6x83, 8);
|
||||
|
||||
@@ -5192,13 +5234,18 @@ struct G_LevelUpAllTechniques_6x9B {
|
||||
parray<uint8_t, 3> unused;
|
||||
} __packed_ws__(G_LevelUpAllTechniques_6x9B, 8);
|
||||
|
||||
// 6x9C: Set enemy low game flags (not valid on Episode 3)
|
||||
// This command only has an effect in Ultimate mode; it sets the low 6 bits of game_flags (those that match 0x3F).
|
||||
// 6x9C: Set enemy status effect flags (not valid on Episode 3)
|
||||
// This command only has an effect in Ultimate mode; it sets the low 6 bits of game_flags (those that match 0x3F). This
|
||||
// essentially separates status effects from the 6x0A command. It's not clear why Sega did this only in Ultimate mode.
|
||||
|
||||
struct G_SetEnemyLowGameFlagsUltimate_6x9C {
|
||||
G_EntityIDHeader header;
|
||||
// A virtual function is called on the enemy if low_game_flags is equal to any of 0x02, 0x04, 0x10, or 0x20.
|
||||
le_uint32_t low_game_flags = 0;
|
||||
// This field is expected to have one of these values (it should not actually be treated as a bit field):
|
||||
// 0x00000002 = add paralysis status
|
||||
// 0x00000004 = add shock status
|
||||
// 0x00000010 = add confuse status
|
||||
// 0x00000020 = add freeze status
|
||||
le_uint32_t status_effect_flags = 0;
|
||||
} __packed_ws__(G_SetEnemyLowGameFlagsUltimate_6x9C, 8);
|
||||
|
||||
// 6x9D: Set dead flag (Challenge mode; not valid on Episode 3)
|
||||
@@ -5313,11 +5360,9 @@ struct G_GolDragonBossActionsT_6xA8 {
|
||||
F32T<BE> z = 0.0f;
|
||||
uint8_t unknown_a5 = 0;
|
||||
parray<uint8_t, 3> unused;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws_be__(G_GolDragonBossActionsT_6xA8, 0x18);
|
||||
using G_GolDragonBossActions_XB_BB_6xA8 = G_GolDragonBossActionsT_6xA8<false>;
|
||||
using G_GolDragonBossActions_GC_6xA8 = G_GolDragonBossActionsT_6xA8<true>;
|
||||
check_struct_size(G_GolDragonBossActions_XB_BB_6xA8, 0x18);
|
||||
check_struct_size(G_GolDragonBossActions_GC_6xA8, 0x18);
|
||||
|
||||
// 6xA9: Barba Ray boss actions (not valid on pre-V3 or Episode 3)
|
||||
|
||||
@@ -5389,25 +5434,20 @@ struct G_SetAnimationState_6xAE {
|
||||
G_ClientIDHeader header;
|
||||
le_uint16_t animation_number = 0;
|
||||
le_uint16_t unused = 0;
|
||||
// This field contains the flags field on the sender's TObjPlayer object. If the bit 04000000 is set in this field,
|
||||
// then (flags & 1C000000) is or'ed into the TObjPlayer's flags field. All other bits are ignored.
|
||||
le_uint32_t flags = 0;
|
||||
// This field contains the player_flags field on the sender's TObjPlayer object. If the bit 04000000 is set in this
|
||||
// field, then (flags & 1C000000) is or'ed into the TObjPlayer's player_flags field on the receiver's end; all other
|
||||
// bits are ignored. (For the meanings of these bits, see Parsed6x70Data::convert_player_flags.)
|
||||
le_uint32_t player_flags = 0;
|
||||
le_float animation_timer = 0;
|
||||
} __packed_ws__(G_SetAnimationState_6xAE, 0x10);
|
||||
|
||||
// 6xAF: Turn lobby chair (not valid on pre-V3 or GC Trial Edition) (protected on V3/V4)
|
||||
|
||||
struct G_TurnLobbyChair_6xAF {
|
||||
G_ClientIDHeader header;
|
||||
le_uint32_t angle = 0; // In range [0x0000, 0xFFFF]
|
||||
} __packed_ws__(G_TurnLobbyChair_6xAF, 8);
|
||||
|
||||
// 6xB0: Move lobby chair (not valid on pre-V3 or GC Trial Edition) (protected on V3/V4)
|
||||
|
||||
struct G_MoveLobbyChair_6xB0 {
|
||||
struct G_TurnOrMoveLobbyChair_6xAF_6xB0 {
|
||||
G_ClientIDHeader header;
|
||||
le_uint32_t unknown_a1 = 0;
|
||||
} __packed_ws__(G_MoveLobbyChair_6xB0, 8);
|
||||
le_uint32_t angle = 0; // In range [0x0000, 0xFFFF]
|
||||
} __packed_ws__(G_TurnOrMoveLobbyChair_6xAF_6xB0, 8);
|
||||
|
||||
// 6xB1: Unknown (not valid on pre-V3 or GC Trial Edition)
|
||||
// This subcommand is completely ignored.
|
||||
@@ -5673,7 +5713,7 @@ struct G_BankAction_BB_6xBD {
|
||||
G_UnusedHeader header;
|
||||
le_uint32_t item_id = 0;
|
||||
le_uint32_t meseta_amount = 0;
|
||||
uint8_t action = 0; // 0 = deposit, 1 = take, 3 = done (close bank window)
|
||||
uint8_t action = 0; // 0 = deposit, 1 = take, 2 = unknown (compact contents?), 3 = commit (close bank window)
|
||||
uint8_t item_amount = 0;
|
||||
le_uint16_t item_index = 0; // 0xFFFF = meseta
|
||||
} __packed_ws__(G_BankAction_BB_6xBD, 0x10);
|
||||
@@ -6059,7 +6099,7 @@ struct G_SetMesetaSlotPrizeResult_BB_6xE3 {
|
||||
ItemData item;
|
||||
} __packed_ws__(G_SetMesetaSlotPrizeResult_BB_6xE3, 0x18);
|
||||
|
||||
// 6xE4: Invalid subcommand (but used as an extension; see end of this file)
|
||||
// 6xE4: Invalid subcommand (but used as a newserv extension; see end of this file)
|
||||
// 6xE5: Invalid subcommand
|
||||
// 6xE6: Invalid subcommand
|
||||
// 6xE7: Invalid subcommand
|
||||
@@ -6325,7 +6365,7 @@ struct G_SetPlayerDeck_Ep3_CAx14 {
|
||||
|
||||
struct G_HardResetServerState_Ep3_CAx15 {
|
||||
G_CardServerDataCommandHeader header = {0xB3, sizeof(G_HardResetServerState_Ep3_CAx15) / 4, 0, 0x15, 0, 0, 0, 0, 0};
|
||||
// No arguments
|
||||
// No arguments.
|
||||
} __packed_ws__(G_HardResetServerState_Ep3_CAx15, 0x10);
|
||||
|
||||
// 6xB5x17: Unknown
|
||||
@@ -6333,7 +6373,7 @@ struct G_HardResetServerState_Ep3_CAx15 {
|
||||
|
||||
struct G_Unknown_Ep3_6xB5x17 {
|
||||
G_CardBattleCommandHeader header = {0xB5, sizeof(G_Unknown_Ep3_6xB5x17) / 4, 0, 0x17, 0, 0, 0};
|
||||
// No arguments
|
||||
// No arguments.
|
||||
} __packed_ws__(G_Unknown_Ep3_6xB5x17, 8);
|
||||
|
||||
// 6xB5x1A: Force disconnect
|
||||
@@ -6343,7 +6383,7 @@ struct G_Unknown_Ep3_6xB5x17 {
|
||||
|
||||
struct G_ForceDisconnect_Ep3_6xB5x1A {
|
||||
G_CardBattleCommandHeader header = {0xB5, sizeof(G_ForceDisconnect_Ep3_6xB5x1A) / 4, 0, 0x1A, 0, 0, 0};
|
||||
// No arguments
|
||||
// No arguments.
|
||||
} __packed_ws__(G_ForceDisconnect_Ep3_6xB5x1A, 8);
|
||||
|
||||
// 6xB3x1B / CAx1B: Set player name
|
||||
@@ -6416,7 +6456,7 @@ struct G_EndBattle_Ep3_CAx21 {
|
||||
|
||||
struct G_Unknown_Ep3_6xB4x22 {
|
||||
G_CardBattleCommandHeader header = {0xB4, sizeof(G_Unknown_Ep3_6xB4x22) / 4, 0, 0x22, 0, 0, 0};
|
||||
// No arguments
|
||||
// No arguments.
|
||||
} __packed_ws__(G_Unknown_Ep3_6xB4x22, 8);
|
||||
|
||||
// 6xB4x23: Unknown
|
||||
@@ -6559,7 +6599,7 @@ struct G_SetDeckInBattleSetupMenu_Ep3_6xB5x2F {
|
||||
|
||||
struct G_Unused_Ep3_6xB5x30 {
|
||||
G_CardBattleCommandHeader header = {0xB5, sizeof(G_Unused_Ep3_6xB5x30) / 4, 0, 0x30, 0, 0, 0};
|
||||
// No arguments
|
||||
// No arguments.
|
||||
} __packed_ws__(G_Unused_Ep3_6xB5x30, 8);
|
||||
|
||||
// 6xB5x31: Confirm deck selection
|
||||
@@ -7064,7 +7104,7 @@ struct G_RejectBattleStartRequest_Ep3_6xB4x53 {
|
||||
// 30 (C->S): Extended player info
|
||||
// Requested with the GetExtendedPlayerInfo patch. Format depends on version:
|
||||
// DC v2: PSODCV2CharacterFile
|
||||
// GC v3: PSOGCCharacterFile::Character
|
||||
// GC v3: PSOGCCharacterFile::Character (including big-endian fields, unlike 61)
|
||||
// XB v3: PSOXBCharacterFile::Character
|
||||
|
||||
// 6xE4: Increment enemy damage
|
||||
|
||||
@@ -37,6 +37,14 @@ struct VectorXZF {
|
||||
inline double norm2() const {
|
||||
return ((this->x * this->x) + (this->z * this->z));
|
||||
}
|
||||
inline double dist(const VectorXZF& other) const {
|
||||
return sqrt(this->dist2(other));
|
||||
}
|
||||
inline double dist2(const VectorXZF& other) const {
|
||||
double x = this->x - other.x;
|
||||
double z = this->z - other.z;
|
||||
return ((x * x) + (z * z));
|
||||
}
|
||||
|
||||
inline VectorXZF rotate_y(double angle) const {
|
||||
double s = sin(angle);
|
||||
@@ -123,11 +131,9 @@ struct ArrayRefT {
|
||||
/* 00 */ U32T<BE> count;
|
||||
/* 04 */ U32T<BE> offset;
|
||||
/* 08 */
|
||||
} __attribute__((packed));
|
||||
} __packed_ws_be__(ArrayRefT, 8);
|
||||
using ArrayRef = ArrayRefT<false>;
|
||||
using ArrayRefBE = ArrayRefT<true>;
|
||||
check_struct_size(ArrayRef, 8);
|
||||
check_struct_size(ArrayRefBE, 8);
|
||||
|
||||
template <bool BE>
|
||||
struct RELFileFooterT {
|
||||
@@ -151,8 +157,6 @@ struct RELFileFooterT {
|
||||
parray<U32T<BE>, 2> unused1;
|
||||
U32T<BE> root_offset = 0;
|
||||
parray<U32T<BE>, 3> unused2;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws_be__(RELFileFooterT, 0x20);
|
||||
using RELFileFooter = RELFileFooterT<false>;
|
||||
using RELFileFooterBE = RELFileFooterT<true>;
|
||||
check_struct_size(RELFileFooter, 0x20);
|
||||
check_struct_size(RELFileFooterBE, 0x20);
|
||||
|
||||
+150
-130
@@ -134,67 +134,57 @@ CommonItemSet::Table::Table(std::shared_ptr<const Table> prev_table, const phosg
|
||||
parse_field("UnitMaxStarsTable", this->unit_max_stars_table, prev_table ? &prev_table->unit_max_stars_table : nullptr);
|
||||
parse_field("BoxItemClassProbTable", this->box_item_class_prob_table, prev_table ? &prev_table->box_item_class_prob_table : nullptr);
|
||||
|
||||
const auto* enemy_meseta_ranges_json = json.count("EnemyMesetaRanges") ? &json.at("EnemyMesetaRanges").as_dict() : nullptr;
|
||||
const auto* enemy_type_drop_probs_json = json.count("EnemyTypeDropProbs") ? &json.at("EnemyTypeDropProbs").as_dict() : nullptr;
|
||||
const auto* enemy_item_classes_json = json.count("EnemyItemClasses") ? &json.at("EnemyItemClasses").as_dict() : nullptr;
|
||||
if (enemy_item_classes_json) {
|
||||
// Unspecified is 0xFF, not 0, unlike the other enemy-indexed arrays (except for [0], apparently... sigh)
|
||||
this->enemy_item_classes[0] = 0;
|
||||
this->enemy_item_classes.clear_after(1, 0xFF);
|
||||
if (json.count("EnemyMesetaRanges")) {
|
||||
const auto& dict = json.at("EnemyMesetaRanges").as_dict();
|
||||
for (auto enemy_type : phosg::EnumRange<EnemyType>()) {
|
||||
try {
|
||||
from_json_into(*dict.at(phosg::name_for_enum(enemy_type)), this->enemy_type_meseta_ranges[enemy_type]);
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this->enemy_type_meseta_ranges = prev_table->enemy_type_meseta_ranges;
|
||||
}
|
||||
for (size_t z = 0; z < NUM_RT_INDEXES_V4; z++) {
|
||||
auto types = enemy_types_for_rare_table_index(this->episode, z);
|
||||
vector<string> names;
|
||||
if (types.empty()) {
|
||||
names.emplace_back(std::format("!{:02X}", z));
|
||||
} else {
|
||||
for (auto type : types) {
|
||||
names.emplace_back(phosg::name_for_enum(type));
|
||||
|
||||
if (json.count("EnemyTypeDropProbs")) {
|
||||
const auto& dict = json.at("EnemyTypeDropProbs").as_dict();
|
||||
for (auto enemy_type : phosg::EnumRange<EnemyType>()) {
|
||||
try {
|
||||
this->enemy_type_drop_probs[enemy_type] = dict.at(phosg::name_for_enum(enemy_type))->as_int();
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
}
|
||||
for (const auto& name : names) {
|
||||
if (enemy_meseta_ranges_json) {
|
||||
try {
|
||||
from_json_into(*enemy_meseta_ranges_json->at(name), this->enemy_meseta_ranges[z]);
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
} else if (prev_table) {
|
||||
this->enemy_meseta_ranges = prev_table->enemy_meseta_ranges;
|
||||
}
|
||||
if (enemy_type_drop_probs_json) {
|
||||
try {
|
||||
this->enemy_type_drop_probs[z] = enemy_type_drop_probs_json->at(name)->as_int();
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
} else if (prev_table) {
|
||||
this->enemy_type_drop_probs = prev_table->enemy_type_drop_probs;
|
||||
}
|
||||
if (enemy_item_classes_json) {
|
||||
try {
|
||||
this->enemy_item_classes[z] = enemy_item_classes_json->at(name)->as_int();
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
} else if (prev_table) {
|
||||
this->enemy_item_classes = prev_table->enemy_item_classes;
|
||||
} else {
|
||||
this->enemy_type_drop_probs = prev_table->enemy_type_drop_probs;
|
||||
}
|
||||
|
||||
if (json.count("EnemyItemClasses")) {
|
||||
const auto& dict = json.at("EnemyItemClasses").as_dict();
|
||||
for (auto enemy_type : phosg::EnumRange<EnemyType>()) {
|
||||
try {
|
||||
this->enemy_type_item_classes[enemy_type] = dict.at(phosg::name_for_enum(enemy_type))->as_int();
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this->enemy_type_item_classes = prev_table->enemy_type_item_classes;
|
||||
}
|
||||
}
|
||||
|
||||
static const char* name_for_common_item_class(uint8_t item_class) {
|
||||
switch (item_class) {
|
||||
case 0x00:
|
||||
return "WEAPON ";
|
||||
return "WEAPON";
|
||||
case 0x01:
|
||||
return "ARMOR ";
|
||||
return "ARMOR";
|
||||
case 0x02:
|
||||
return "SHIELD ";
|
||||
return "SHIELD";
|
||||
case 0x03:
|
||||
return "UNIT ";
|
||||
return "UNIT";
|
||||
case 0x04:
|
||||
return "TOOL ";
|
||||
return "TOOL";
|
||||
case 0x05:
|
||||
return "MESETA ";
|
||||
return "MESETA";
|
||||
case 0x06:
|
||||
return "NOTHING";
|
||||
default:
|
||||
@@ -203,42 +193,29 @@ static const char* name_for_common_item_class(uint8_t item_class) {
|
||||
}
|
||||
|
||||
void CommonItemSet::Table::print(FILE* stream) const {
|
||||
const auto& meseta_ranges = this->enemy_meseta_ranges;
|
||||
const auto& meseta_ranges = this->enemy_type_meseta_ranges;
|
||||
const auto& drop_probs = this->enemy_type_drop_probs;
|
||||
const auto& item_classes = this->enemy_item_classes;
|
||||
const auto& item_classes = this->enemy_type_item_classes;
|
||||
phosg::fwrite_fmt(stream, "Enemy tables:\n");
|
||||
phosg::fwrite_fmt(stream, " ## $LOW $HIGH DAR% ITEM ENEMIES\n");
|
||||
for (size_t z = 0; z < NUM_RT_INDEXES_V4; z++) {
|
||||
string enemies_str;
|
||||
for (EnemyType enemy_type : enemy_types_for_rare_table_index(this->episode, z)) {
|
||||
if (!enemies_str.empty()) {
|
||||
enemies_str += ", ";
|
||||
}
|
||||
enemies_str += phosg::name_for_enum(enemy_type);
|
||||
}
|
||||
if (drop_probs[z] || !enemies_str.empty()) {
|
||||
phosg::fwrite_fmt(stream, " {:02X} {:5} {:5} {:3}% {:02X}:{} {}\n",
|
||||
z, meseta_ranges[z].min, meseta_ranges[z].max, drop_probs[z], item_classes[z],
|
||||
name_for_common_item_class(item_classes[z]), enemies_str);
|
||||
} else {
|
||||
phosg::fwrite_fmt(stream, " {:02X} ----- ----- 0% --\n", z);
|
||||
phosg::fwrite_fmt(stream, " ##:ENEMY $LOW $HIGH DAR% ITEM\n");
|
||||
for (auto enemy_type : phosg::EnumRange<EnemyType>()) {
|
||||
const auto& def = type_definition_for_enemy(enemy_type);
|
||||
try {
|
||||
const auto& meseta_range = meseta_ranges.at(enemy_type);
|
||||
const auto& drop_prob = drop_probs.at(enemy_type);
|
||||
const auto& item_class = item_classes.at(enemy_type);
|
||||
phosg::fwrite_fmt(stream, " {:02X}:{:<23} {:5} {:5} {:3}% {:02X}:{:<7}\n",
|
||||
def.rt_index, phosg::name_for_enum(enemy_type),
|
||||
meseta_range.min, meseta_range.max, drop_prob, item_class,
|
||||
name_for_common_item_class(item_class));
|
||||
} catch (const out_of_range&) {
|
||||
phosg::fwrite_fmt(stream, " {:02X}:{:<23} ----- ----- ---- --:-------\n",
|
||||
def.rt_index, phosg::name_for_enum(enemy_type));
|
||||
}
|
||||
}
|
||||
|
||||
static const array<const char*, 12> base_weapon_type_names = {
|
||||
"SABER ",
|
||||
"SWORD ",
|
||||
"DAGGER ",
|
||||
"PARTISAN",
|
||||
"SLICER ",
|
||||
"HANDGUN ",
|
||||
"RIFLE ",
|
||||
"MECHGUN ",
|
||||
"SHOT ",
|
||||
"CANE ",
|
||||
"ROD ",
|
||||
"WAND ",
|
||||
};
|
||||
"SABER", "SWORD", "DAGGER", "PARTISAN", "SLICER", "HANDGUN", "RIFLE", "MECHGUN", "SHOT", "CANE", "ROD", "WAND"};
|
||||
phosg::fwrite_fmt(stream, "Base weapon config:\n");
|
||||
phosg::fwrite_fmt(stream, " TYPE PROB [SB AL] FLOORS\n");
|
||||
for (size_t z = 0; z < 12; z++) {
|
||||
@@ -256,7 +233,7 @@ void CommonItemSet::Table::print(FILE* stream) const {
|
||||
floor_to_class[x] = this->subtype_base_table[z] + (x / this->subtype_area_length_table[z]);
|
||||
}
|
||||
}
|
||||
phosg::fwrite_fmt(stream, " {:02X}:{} {:3}% [{:02X} {:02X}] {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X}\n",
|
||||
phosg::fwrite_fmt(stream, " {:02X}:{:<8} {:3}% [{:02X} {:02X}] {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X}\n",
|
||||
z, base_weapon_type_names[z], this->base_weapon_type_prob_table[z],
|
||||
this->subtype_base_table[z], this->subtype_area_length_table[z],
|
||||
floor_to_class[0], floor_to_class[1], floor_to_class[2], floor_to_class[3], floor_to_class[4],
|
||||
@@ -413,20 +390,50 @@ void CommonItemSet::Table::print_diff(FILE* stream, const Table& other) const {
|
||||
phosg::format_data_string(&this->armor_slot_count_prob_table, sizeof(this->armor_slot_count_prob_table)),
|
||||
phosg::format_data_string(&other.armor_slot_count_prob_table, sizeof(other.armor_slot_count_prob_table)));
|
||||
}
|
||||
if (this->enemy_meseta_ranges != other.enemy_meseta_ranges) {
|
||||
phosg::fwrite_fmt(stream, "> enemy_meseta_ranges: {} -> {}\n",
|
||||
phosg::format_data_string(&this->enemy_meseta_ranges, sizeof(this->enemy_meseta_ranges)),
|
||||
phosg::format_data_string(&other.enemy_meseta_ranges, sizeof(other.enemy_meseta_ranges)));
|
||||
|
||||
auto format_enemy_range_table = [&](const std::unordered_map<EnemyType, Range<uint16_t>>& table) -> std::string {
|
||||
string ret = "";
|
||||
for (auto enemy_type : phosg::EnumRange<EnemyType>()) {
|
||||
try {
|
||||
const auto& range = table.at(enemy_type);
|
||||
if (!ret.empty()) {
|
||||
ret += ",";
|
||||
}
|
||||
ret += std::format("{}=[{},{}]", phosg::name_for_enum(enemy_type), range.min, range.max);
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
auto format_enemy_u8_table = [&](const std::unordered_map<EnemyType, uint8_t>& table) -> std::string {
|
||||
string ret = "";
|
||||
for (auto enemy_type : phosg::EnumRange<EnemyType>()) {
|
||||
try {
|
||||
uint8_t value = table.at(enemy_type);
|
||||
if (!ret.empty()) {
|
||||
ret += ",";
|
||||
}
|
||||
ret += std::format("{}={}", phosg::name_for_enum(enemy_type), value);
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
|
||||
if (this->enemy_type_meseta_ranges != other.enemy_type_meseta_ranges) {
|
||||
phosg::fwrite_fmt(stream, "> enemy_type_meseta_ranges: {} -> {}\n",
|
||||
format_enemy_range_table(this->enemy_type_meseta_ranges),
|
||||
format_enemy_range_table(other.enemy_type_meseta_ranges));
|
||||
}
|
||||
if (this->enemy_type_drop_probs != other.enemy_type_drop_probs) {
|
||||
phosg::fwrite_fmt(stream, "> enemy_type_drop_probs: {} -> {}\n",
|
||||
phosg::format_data_string(&this->enemy_type_drop_probs, sizeof(this->enemy_type_drop_probs)),
|
||||
phosg::format_data_string(&other.enemy_type_drop_probs, sizeof(other.enemy_type_drop_probs)));
|
||||
format_enemy_u8_table(this->enemy_type_drop_probs),
|
||||
format_enemy_u8_table(other.enemy_type_drop_probs));
|
||||
}
|
||||
if (this->enemy_item_classes != other.enemy_item_classes) {
|
||||
phosg::fwrite_fmt(stream, "> enemy_item_classes: {} -> {}\n",
|
||||
phosg::format_data_string(&this->enemy_item_classes, sizeof(this->enemy_item_classes)),
|
||||
phosg::format_data_string(&other.enemy_item_classes, sizeof(other.enemy_item_classes)));
|
||||
if (this->enemy_type_item_classes != other.enemy_type_item_classes) {
|
||||
phosg::fwrite_fmt(stream, "> enemy_type_item_classes: {} -> {}\n",
|
||||
format_enemy_u8_table(this->enemy_type_item_classes),
|
||||
format_enemy_u8_table(other.enemy_type_item_classes));
|
||||
}
|
||||
if (this->box_meseta_ranges != other.box_meseta_ranges) {
|
||||
phosg::fwrite_fmt(stream, "> box_meseta_ranges: {} -> {}\n",
|
||||
@@ -517,44 +524,45 @@ phosg::JSON CommonItemSet::Table::json(std::shared_ptr<const Table> prev_table)
|
||||
ret.emplace("ArmorSlotCountProbTable", to_json(this->armor_slot_count_prob_table));
|
||||
}
|
||||
|
||||
bool needs_enemy_meseta_ranges = (!prev_table || (this->enemy_meseta_ranges != prev_table->enemy_meseta_ranges));
|
||||
bool needs_enemy_type_drop_probs = (!prev_table || (this->enemy_type_drop_probs != prev_table->enemy_type_drop_probs));
|
||||
bool needs_enemy_item_classes = (!prev_table || (this->enemy_item_classes != prev_table->enemy_item_classes));
|
||||
if (needs_enemy_meseta_ranges || needs_enemy_type_drop_probs || needs_enemy_item_classes) {
|
||||
phosg::JSON enemy_meseta_ranges_json = phosg::JSON::dict();
|
||||
bool needs_enemy_type_meseta_ranges = (!prev_table ||
|
||||
(this->enemy_type_meseta_ranges != prev_table->enemy_type_meseta_ranges));
|
||||
bool needs_enemy_type_drop_probs = (!prev_table ||
|
||||
(this->enemy_type_drop_probs != prev_table->enemy_type_drop_probs));
|
||||
bool needs_enemy_type_item_classes = (!prev_table ||
|
||||
(this->enemy_type_item_classes != prev_table->enemy_type_item_classes));
|
||||
if (needs_enemy_type_meseta_ranges || needs_enemy_type_drop_probs || needs_enemy_type_item_classes) {
|
||||
phosg::JSON enemy_type_meseta_ranges_json = phosg::JSON::dict();
|
||||
phosg::JSON enemy_type_drop_probs_json = phosg::JSON::dict();
|
||||
phosg::JSON enemy_item_classes_json = phosg::JSON::dict();
|
||||
for (size_t z = 0; z < NUM_RT_INDEXES_V4; z++) {
|
||||
auto types = enemy_types_for_rare_table_index(this->episode, z);
|
||||
vector<string> names;
|
||||
if (types.empty()) {
|
||||
names.emplace_back(std::format("!{:02X}", z));
|
||||
} else {
|
||||
for (auto type : types) {
|
||||
names.emplace_back(phosg::name_for_enum(type));
|
||||
phosg::JSON enemy_type_item_classes_json = phosg::JSON::dict();
|
||||
for (auto enemy_type : phosg::EnumRange<EnemyType>()) {
|
||||
auto name = phosg::name_for_enum(enemy_type);
|
||||
if (needs_enemy_type_meseta_ranges) {
|
||||
try {
|
||||
enemy_type_meseta_ranges_json.emplace(name, to_json(this->enemy_type_meseta_ranges.at(enemy_type)));
|
||||
} catch (const std::out_of_range&) {
|
||||
}
|
||||
}
|
||||
for (const auto& name : names) {
|
||||
if (needs_enemy_meseta_ranges && (!types.empty() || !this->enemy_meseta_ranges[z].empty())) {
|
||||
enemy_meseta_ranges_json.emplace(name, to_json(this->enemy_meseta_ranges[z]));
|
||||
if (needs_enemy_type_drop_probs) {
|
||||
try {
|
||||
enemy_type_drop_probs_json.emplace(name, this->enemy_type_drop_probs.at(enemy_type));
|
||||
} catch (const std::out_of_range&) {
|
||||
}
|
||||
if (needs_enemy_type_drop_probs && (!types.empty() || this->enemy_type_drop_probs[z])) {
|
||||
enemy_type_drop_probs_json.emplace(name, this->enemy_type_drop_probs[z]);
|
||||
}
|
||||
if (needs_enemy_item_classes && (!types.empty() || (this->enemy_item_classes[z] != ((z == 0) ? 0x00 : 0xFF)))) {
|
||||
enemy_item_classes_json.emplace(name, this->enemy_item_classes[z]);
|
||||
}
|
||||
if (needs_enemy_type_item_classes) {
|
||||
try {
|
||||
enemy_type_item_classes_json.emplace(name, this->enemy_type_item_classes.at(enemy_type));
|
||||
} catch (const std::out_of_range&) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (needs_enemy_meseta_ranges) {
|
||||
ret.emplace("EnemyMesetaRanges", std::move(enemy_meseta_ranges_json));
|
||||
if (needs_enemy_type_meseta_ranges) {
|
||||
ret.emplace("EnemyMesetaRanges", std::move(enemy_type_meseta_ranges_json));
|
||||
}
|
||||
if (needs_enemy_type_drop_probs) {
|
||||
ret.emplace("EnemyTypeDropProbs", std::move(enemy_type_drop_probs_json));
|
||||
}
|
||||
if (needs_enemy_item_classes) {
|
||||
ret.emplace("EnemyItemClasses", std::move(enemy_item_classes_json));
|
||||
if (needs_enemy_type_item_classes) {
|
||||
ret.emplace("EnemyItemClasses", std::move(enemy_type_item_classes_json));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -705,13 +713,27 @@ void CommonItemSet::Table::parse_itempt_t(const phosg::StringReader& r, bool is_
|
||||
this->grind_prob_table = r.pget<parray<parray<uint8_t, 4>, 9>>(offsets.grind_prob_table_offset);
|
||||
this->armor_shield_type_index_prob_table = r.pget<parray<uint8_t, 0x05>>(offsets.armor_shield_type_index_prob_table_offset);
|
||||
this->armor_slot_count_prob_table = r.pget<parray<uint8_t, 0x05>>(offsets.armor_slot_count_prob_table_offset);
|
||||
const auto& data = r.pget<parray<Range<U16T<BE>>, NUM_RT_INDEXES_V3>>(offsets.enemy_meseta_ranges_offset);
|
||||
for (size_t z = 0; z < data.size(); z++) {
|
||||
this->enemy_meseta_ranges[z] = Range<uint16_t>{data[z].min, data[z].max};
|
||||
const auto& enemy_rt_index_meseta_ranges = r.pget<parray<Range<U16T<BE>>, NUM_RT_INDEXES_V3>>(
|
||||
offsets.enemy_rt_index_meseta_ranges_offset);
|
||||
const auto& enemy_rt_index_drop_probs = r.pget<parray<uint8_t, NUM_RT_INDEXES_V3>>(
|
||||
offsets.enemy_rt_index_drop_probs_offset);
|
||||
const auto& enemy_rt_index_item_classes = r.pget<parray<uint8_t, NUM_RT_INDEXES_V3>>(
|
||||
offsets.enemy_rt_index_item_classes_offset);
|
||||
for (auto enemy_type : phosg::EnumRange<EnemyType>()) {
|
||||
const auto& def = type_definition_for_enemy(enemy_type);
|
||||
if (def.valid_in_episode(this->episode) && (def.rt_index < enemy_rt_index_meseta_ranges.size())) {
|
||||
const auto& meseta_range = enemy_rt_index_meseta_ranges[def.rt_index];
|
||||
if (meseta_range.max > 0) {
|
||||
this->enemy_type_meseta_ranges.emplace(enemy_type, Range<uint16_t>{meseta_range.min, meseta_range.max});
|
||||
}
|
||||
if (enemy_rt_index_drop_probs[def.rt_index] > 0) {
|
||||
this->enemy_type_drop_probs.emplace(enemy_type, enemy_rt_index_drop_probs[def.rt_index]);
|
||||
}
|
||||
if (enemy_rt_index_item_classes[def.rt_index] != 0xFF) {
|
||||
this->enemy_type_item_classes.emplace(enemy_type, enemy_rt_index_item_classes[def.rt_index]);
|
||||
}
|
||||
}
|
||||
}
|
||||
this->enemy_type_drop_probs = r.pget<parray<uint8_t, NUM_RT_INDEXES_V3>>(offsets.enemy_type_drop_probs_offset);
|
||||
this->enemy_item_classes = r.pget<parray<uint8_t, NUM_RT_INDEXES_V3>>(offsets.enemy_item_classes_offset);
|
||||
this->enemy_item_classes.clear_after(NUM_RT_INDEXES_V3, 0xFF);
|
||||
{
|
||||
const auto& data = r.pget<parray<Range<U16T<BE>>, 0x0A>>(offsets.box_meseta_ranges_offset);
|
||||
for (size_t z = 0; z < data.size(); z++) {
|
||||
@@ -873,7 +895,7 @@ GSLV3V4CommonItemSet::GSLV3V4CommonItemSet(std::shared_ptr<const std::string> gs
|
||||
section_id);
|
||||
};
|
||||
|
||||
for (Episode episode : ALL_EPISODES_V4) {
|
||||
for (Episode episode : ALL_EPISODES_V3) {
|
||||
for (Difficulty difficulty : ALL_DIFFICULTIES_V234) {
|
||||
for (size_t section_id = 0; section_id < 10; section_id++) {
|
||||
phosg::StringReader r;
|
||||
@@ -898,17 +920,15 @@ GSLV3V4CommonItemSet::GSLV3V4CommonItemSet(std::shared_ptr<const std::string> gs
|
||||
}
|
||||
}
|
||||
|
||||
if (episode != Episode::EP4) {
|
||||
for (Difficulty difficulty : ALL_DIFFICULTIES_V234) {
|
||||
try {
|
||||
auto r = gsl.get_reader(filename_for_table(episode, difficulty, 0, true));
|
||||
auto table = make_shared<Table>(r, is_big_endian, true, episode);
|
||||
for (size_t section_id = 0; section_id < 10; section_id++) {
|
||||
this->tables.emplace(this->key_for_table(episode, GameMode::CHALLENGE, difficulty, section_id), table);
|
||||
}
|
||||
} catch (const out_of_range&) {
|
||||
// GC NTE doesn't have Ep2 challenge; just skip adding the table
|
||||
for (Difficulty difficulty : ALL_DIFFICULTIES_V234) {
|
||||
try {
|
||||
auto r = gsl.get_reader(filename_for_table(episode, difficulty, 0, true));
|
||||
auto table = make_shared<Table>(r, is_big_endian, true, episode);
|
||||
for (size_t section_id = 0; section_id < 10; section_id++) {
|
||||
this->tables.emplace(this->key_for_table(episode, GameMode::CHALLENGE, difficulty, section_id), table);
|
||||
}
|
||||
} catch (const out_of_range&) {
|
||||
// GC NTE doesn't have Ep2 challenge; just skip adding the table
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+12
-14
@@ -42,9 +42,10 @@ public:
|
||||
parray<parray<uint8_t, 4>, 9> grind_prob_table;
|
||||
parray<uint8_t, 0x05> armor_shield_type_index_prob_table;
|
||||
parray<uint8_t, 0x05> armor_slot_count_prob_table;
|
||||
parray<Range<uint16_t>, NUM_RT_INDEXES_V4> enemy_meseta_ranges;
|
||||
parray<uint8_t, NUM_RT_INDEXES_V4> enemy_type_drop_probs;
|
||||
parray<uint8_t, NUM_RT_INDEXES_V4> enemy_item_classes;
|
||||
// Note: PSO originally uses arrays indexed by rt_index here, but we index enemies by the EnemyType enum instead
|
||||
std::unordered_map<EnemyType, Range<uint16_t>> enemy_type_meseta_ranges;
|
||||
std::unordered_map<EnemyType, uint8_t> enemy_type_drop_probs;
|
||||
std::unordered_map<EnemyType, uint8_t> enemy_type_item_classes;
|
||||
parray<Range<uint16_t>, 0x0A> box_meseta_ranges;
|
||||
bool has_rare_bonus_value_prob_table;
|
||||
parray<parray<uint16_t, 6>, 0x17> bonus_value_prob_table;
|
||||
@@ -126,17 +127,17 @@ public:
|
||||
// V2/V3: -> parray<uint8_t, 0x05>
|
||||
/* 14 */ U32T<BE> armor_slot_count_prob_table_offset;
|
||||
|
||||
// This array (indexed by enemy_type) specifies the range of meseta values that each enemy can drop.
|
||||
// This array (indexed by rt_index) specifies the range of meseta values that each enemy can drop.
|
||||
// V2/V3: -> parray<Range<U16T>, NUM_RT_INDEXES_V3>
|
||||
/* 18 */ U32T<BE> enemy_meseta_ranges_offset;
|
||||
/* 18 */ U32T<BE> enemy_rt_index_meseta_ranges_offset;
|
||||
|
||||
// Each byte in this table (indexed by enemy_type) represents the percent chance that the enemy drops anything at
|
||||
// Each byte in this table (indexed by rt_index) represents the percent chance that the enemy drops anything at
|
||||
// all. (This check is done before the rare drop check, so the chance of getting a rare item from an enemy is
|
||||
// essentially this probability multiplied by the rare drop rate.)
|
||||
// V2/V3: -> parray<uint8_t, NUM_RT_INDEXES_V3>
|
||||
/* 1C */ U32T<BE> enemy_type_drop_probs_offset;
|
||||
/* 1C */ U32T<BE> enemy_rt_index_drop_probs_offset;
|
||||
|
||||
// Each byte in this table (indexed by enemy_type) represents the class of item that can drop. The values are:
|
||||
// Each byte in this table (indexed by rt_index) represents the class of item that can drop. The values are:
|
||||
// 00 = weapon
|
||||
// 01 = armor
|
||||
// 02 = shield
|
||||
@@ -145,7 +146,7 @@ public:
|
||||
// 05 = meseta
|
||||
// Anything else = no item
|
||||
// V2/V3: -> parray<uint8_t, NUM_RT_INDEXES_V3>
|
||||
/* 20 */ U32T<BE> enemy_item_classes_offset;
|
||||
/* 20 */ U32T<BE> enemy_rt_index_item_classes_offset;
|
||||
|
||||
// This table (indexed by area - 1) specifies the ranges of meseta values that can drop from boxes.
|
||||
// V2/V3: -> parray<Range<U16T>, 0x0A>
|
||||
@@ -229,7 +230,7 @@ public:
|
||||
// This index probability table determines which type of items drop from boxes. The table is indexed as
|
||||
// [item_class][area - 1], with item_class as the result value (that is, in the example below, the game looks at
|
||||
// a single column and sums the values going down, then the chosen item class is one of the row indexes based on
|
||||
// the weight values in the column.) The resulting value has the same meaning as in enemy_item_classes above.
|
||||
// the weight values in the column.) The resulting value has the same meaning as in enemy_rt_index_item_classes.
|
||||
// For example, this array might look like the following:
|
||||
// [07 07 08 08 06 07 08 09 09 0A] // Chances per area of a weapon drop
|
||||
// [02 02 02 02 03 02 02 02 03 03] // Chances per area of an armor drop
|
||||
@@ -243,11 +244,9 @@ public:
|
||||
/* 50 */ U32T<BE> box_item_class_prob_table_offset;
|
||||
|
||||
// There are several unused fields here.
|
||||
} __attribute__((packed));
|
||||
} __packed_ws_be__(OffsetsT, 0x54);
|
||||
using Offsets = OffsetsT<false>;
|
||||
using OffsetsBE = OffsetsT<true>;
|
||||
check_struct_size(Offsets, 0x54);
|
||||
check_struct_size(OffsetsBE, 0x54);
|
||||
};
|
||||
|
||||
bool operator==(const CommonItemSet& other) const = default;
|
||||
@@ -336,7 +335,6 @@ public:
|
||||
ValueT value;
|
||||
WeightT weight;
|
||||
} __attribute__((packed));
|
||||
|
||||
using WeightTableEntry8 = WeightTableEntry<uint8_t>;
|
||||
using WeightTableEntry32 = WeightTableEntry<be_uint32_t>;
|
||||
check_struct_size(WeightTableEntry8, 2);
|
||||
|
||||
+2
-1
@@ -13,6 +13,7 @@ static constexpr size_t NUM_RT_INDEXES_V3 = 0x64;
|
||||
static constexpr size_t NUM_RT_INDEXES_V4 = 0x70;
|
||||
|
||||
enum class EnemyType : uint8_t {
|
||||
MIN_VALUE = 0,
|
||||
UNKNOWN = 0,
|
||||
NONE,
|
||||
NON_ENEMY_NPC,
|
||||
@@ -146,7 +147,7 @@ enum class EnemyType : uint8_t {
|
||||
ZOL_GIBBON,
|
||||
ZU_CRATER,
|
||||
ZU_DESERT,
|
||||
MAX_ENEMY_TYPE,
|
||||
MAX_VALUE,
|
||||
};
|
||||
|
||||
struct EnemyTypeDefinition {
|
||||
|
||||
+24
-22
@@ -675,8 +675,8 @@ struct CardDefinition {
|
||||
// - (rate / 10) % 100 (that is, the tens and hundreds decimal places) specify the environment number + 1. For
|
||||
// example, if this field contains 5, then this drop only applies if the battle took place at Molae Venti
|
||||
// (environment number 4). If this field is zero, the drop applies regardless of where the battle took place.
|
||||
// - rate / 1000 (the thousands decimal place) specifies the rarity class. This can be any number in the range [0,
|
||||
// 9], and affects how likely the card is to appear based on the player's level. See below for details.
|
||||
// - (rate / 1000) % 10 (the thousands decimal place) specifies the rarity class. This can be any number in the range
|
||||
// [0, 9], and affects how likely the card is to appear based on the player's level. See below for details.
|
||||
// - rate / 10000 (the ten-thousands decimal place) specifies if the drop rate applies only if the player used a
|
||||
// Hunters deck (1), only if they used an Arkz deck (2), or if they used any deck (0).
|
||||
//
|
||||
@@ -684,16 +684,16 @@ struct CardDefinition {
|
||||
// that applies, the game adds the card ID into an appropriate bucket based on the rarity class. (If both drop rates
|
||||
// for a card apply, the card ID is added twice.) The player's level class is then computed according to the
|
||||
// following table:
|
||||
// 1 2 3 4 5 6 7 8 9 10
|
||||
// CLvOff 1-2 3-4 5-9 10-14 15-19 20-25 26-29 30-39 40-49 50+
|
||||
// CLvOn 1-2 3-4 5-10 11-16 17-23 24-32 33-39 40-49 50-99 100+
|
||||
// Level class 1 2 3 4 5 6 7 8 9 10
|
||||
// CLvOff 1-2 3-4 5-9 10-14 15-19 20-25 26-29 30-39 40-49 50+
|
||||
// CLvOn 1-2 3-4 5-10 11-16 17-23 24-32 33-39 40-49 50-99 100+
|
||||
// For the purposes of this computation, the player's level is used by default (CLvOn or CLvOff), but the map may
|
||||
// override it - see win_level_override and loss_level_override in MapDefinition. This specifies which row in the
|
||||
// following tables will be used.
|
||||
//
|
||||
// Cards are then chosen from the buckets with a weighted distribution according to these tables (row is player's
|
||||
// level class, column is card's rarity class):
|
||||
// Offline:
|
||||
// Offline ---------------------------------------------------------------
|
||||
// LC | RC = 0 1 2 3 4 5 6 7 8 9
|
||||
// 1 | 8000 2000 50
|
||||
// 2 | 6000 3500 500 20
|
||||
@@ -705,7 +705,7 @@ struct CardDefinition {
|
||||
// 8 | 1789 2100 2100 2100 1100 800 10 1
|
||||
// 9 | 14620 20000 21000 22000 13000 9000 300 80
|
||||
// 10 | 133997 190000 200000 200000 150000 120000 5000 1000 2 1
|
||||
// Online:
|
||||
// Online --------------------------------------------------------------------
|
||||
// LC | RC = 0 1 2 3 4 5 6 7 8 9
|
||||
// 1 | 8000 2000 50
|
||||
// 2 | 6000 3500 500 50
|
||||
@@ -729,31 +729,33 @@ struct CardDefinition {
|
||||
// - In the blue pack, the restricted cards must be creature cards.
|
||||
// - In the red pack, the restricted cards must be item cards.
|
||||
// - In the green pack, the restricted cards must be action cards.
|
||||
// - In the black pack, the restricted cards may be any card type.
|
||||
// For example, if you get a B+ rank after winning a battle and pick the green pack, you will always get at least two
|
||||
// action cards.
|
||||
// action cards. The remaining cards can be any type, so this restriction essentially biases the blue, red, and green
|
||||
// packs toward specific card types.
|
||||
//
|
||||
// The game then samples N card IDs from the appropriate buckets (where N is the number chosen above), but for the
|
||||
// first 1 or 2 cards, it applies the restriction described above and re-draws if the card is the wrong type. After
|
||||
// sampling the N card IDs, it sorts them and presents them to the player.
|
||||
// The game then samples N card IDs from the appropriate buckets (where N is the number chosen above based on the
|
||||
// post-battle rank), but for the first 1 or 2 cards, it applies the restriction described above and re-draws if the
|
||||
// card is the wrong type. After sampling the N card IDs, it sorts them and presents them to the player.
|
||||
//
|
||||
// There is one more effect to consider after cards are chosen: cards may randomly transform into VIP cards or into
|
||||
// stronger (and rarer) cards. The chance of each of these occurring is based on the rarity of that card that may
|
||||
// stronger (and rarer) cards. The chance of each of these occurring is based on the rarity of the card that may
|
||||
// transform, and the number of copies of that card which the player already has. In the below table, P(activate) is
|
||||
// the probability that any transformation is attempted at all; if this check passes, the player sees the glow effect
|
||||
// and "The change will occur..." text. P(vip) is the probability that the card becomes a VIP card, after the glow
|
||||
// effect plays. P(rare) is the probability of the card becoming a rarer card after the glow effect. Therefore, the
|
||||
// final probability that a card will transform into a VIP card is P(activate) * P(vip), and the final probability of
|
||||
// transforming into a rarer card is P(activate) * P(rare).
|
||||
// ======== Card rank N4-N1 ======== ======== Card rank R4-R1 ========
|
||||
// Count P(activate) P(rare) P(vip) P(activate) P(rare) P(vip)
|
||||
// 0-4 0% 0% 0% 0% 0% 0%
|
||||
// 5-10 1.923077% 55% 0.5% 2.0408163% 55% 0.5%
|
||||
// 11-16 2.1276595% 60% 0.45454544% 2.2727273% 60% 0.4761905%
|
||||
// 17-24 2.3809524% 70% 0.4347826% 2.5641026% 70% 0.45454544%
|
||||
// 25-32 2.7027028% 70% 0.4% 2.9411765% 70% 0.5%
|
||||
// 33-40 3.125% 80% 0.38461538% 3.448276% 70% 0.5%
|
||||
// 41-52 3.7037037% 80% 0.35714286% 4.1666668% 80% 0.45454544%
|
||||
// 53-99 5% 90% 0.33333334% 5.263158% 90% 0.4347826%
|
||||
// ========= Card rank N4-N1 ========= ========= Card rank R4-R1 =========
|
||||
// Count P(activate) P(rare) P(vip) P(activate) P(rare) P(vip)
|
||||
// 0-4 0% 0% 0% 0% 0% 0%
|
||||
// 5-10 1.923077% 55% 0.5% 2.0408163% 55% 0.5%
|
||||
// 11-16 2.1276595% 60% 0.45454544% 2.2727273% 60% 0.4761905%
|
||||
// 17-24 2.3809524% 70% 0.4347826% 2.5641026% 70% 0.45454544%
|
||||
// 25-32 2.7027028% 70% 0.4% 2.9411765% 70% 0.5%
|
||||
// 33-40 3.125% 80% 0.38461538% 3.448276% 70% 0.5%
|
||||
// 41-52 3.7037037% 80% 0.35714286% 4.1666668% 80% 0.45454544%
|
||||
// 53-99 5% 90% 0.33333334% 5.263158% 90% 0.4347826%
|
||||
//
|
||||
// If a transformation occurs, the card transforms to a card of a different rank. First, the game consults the
|
||||
// following table to determine the rank of the resulting card (original card's rank on the left, new card's rank
|
||||
|
||||
+1
-4
@@ -15,12 +15,9 @@ struct GSLHeaderEntryT {
|
||||
U32T<BE> offset; // In pages, so actual offset is this * 0x800
|
||||
U32T<BE> size;
|
||||
uint64_t unused;
|
||||
} __attribute__((packed));
|
||||
|
||||
} __packed_ws_be__(GSLHeaderEntryT, 0x30);
|
||||
using GSLHeaderEntry = GSLHeaderEntryT<false>;
|
||||
using GSLHeaderEntryBE = GSLHeaderEntryT<true>;
|
||||
check_struct_size(GSLHeaderEntry, 0x30);
|
||||
check_struct_size(GSLHeaderEntryBE, 0x30);
|
||||
|
||||
template <bool BE>
|
||||
void GSLArchive::load_t() {
|
||||
|
||||
@@ -44,6 +44,7 @@ void GameServer::listen(
|
||||
|
||||
shared_ptr<Client> GameServer::connect_channel(shared_ptr<Channel> ch, uint16_t port, ServerBehavior initial_state) {
|
||||
auto c = make_shared<Client>(this->shared_from_this(), ch, initial_state);
|
||||
c->listener_port = port;
|
||||
|
||||
this->log.info_f("Client connected: C-{:X} via TSI-{}-{}-{}",
|
||||
c->id, port, phosg::name_for_enum(ch->version), phosg::name_for_enum(initial_state));
|
||||
@@ -133,6 +134,7 @@ shared_ptr<Client> GameServer::create_client(
|
||||
phosg::TerminalFormat::FG_YELLOW,
|
||||
phosg::TerminalFormat::FG_GREEN);
|
||||
auto c = make_shared<Client>(this->shared_from_this(), channel, listen_sock->behavior);
|
||||
c->listener_port = listen_sock->endpoint.port();
|
||||
this->log.info_f("Client connected: C-{:X} via {}", c->id, listen_sock->name);
|
||||
|
||||
return c;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#include <phosg/Network.hh>
|
||||
#include <string>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
|
||||
#include "GameServer.hh"
|
||||
@@ -54,6 +55,156 @@ HTTPServer::HTTPServer(shared_ptr<ServerState> state)
|
||||
co_return nullptr;
|
||||
});
|
||||
|
||||
this->router.add(HTTPRequest::Method::GET, "/metrics", [this](ArgsT&&) -> RetT {
|
||||
auto version_label = +[](Version v) -> const char* {
|
||||
if (is_patch(v)) {
|
||||
return "patch";
|
||||
} else if (is_v4(v)) {
|
||||
return "v4";
|
||||
} else if (is_v3(v)) {
|
||||
return "v3";
|
||||
} else if (is_v1_or_v2(v)) {
|
||||
return "v2";
|
||||
} else {
|
||||
return "other";
|
||||
}
|
||||
};
|
||||
|
||||
auto escape_label = +[](const string& in) -> string {
|
||||
string out;
|
||||
for (char ch : in) {
|
||||
if (ch == '\\') {
|
||||
out += "\\\\";
|
||||
} else if (ch == '"') {
|
||||
out += "\\\"";
|
||||
} else if (ch == '\n') {
|
||||
out += "\\n";
|
||||
} else {
|
||||
out += ch;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
auto add_metric = [](string& out, const string& name, uint64_t value) -> void {
|
||||
out += name;
|
||||
out += " ";
|
||||
out += std::to_string(value);
|
||||
out += "\n";
|
||||
};
|
||||
|
||||
auto add_metric_1label = [](string& out, const string& name, const string& label_name, const string& label_value, uint64_t value) -> void {
|
||||
out += name;
|
||||
out += "{";
|
||||
out += label_name;
|
||||
out += "=\"";
|
||||
out += label_value;
|
||||
out += "\"} ";
|
||||
out += std::to_string(value);
|
||||
out += "\n";
|
||||
};
|
||||
|
||||
map<string, uint64_t> connected_by_version;
|
||||
map<string, uint64_t> lobby_players_by_version;
|
||||
map<string, uint64_t> game_players_by_version;
|
||||
|
||||
uint64_t connected_total = 0;
|
||||
uint64_t lobbies_total = 0;
|
||||
uint64_t games_total = 0;
|
||||
uint64_t players_in_lobbies_total = 0;
|
||||
uint64_t players_in_games_total = 0;
|
||||
|
||||
for (const auto& c : this->state->game_server->all_clients()) {
|
||||
connected_total++;
|
||||
connected_by_version[version_label(c->version())]++;
|
||||
}
|
||||
|
||||
for (const auto& [_, l] : this->state->id_to_lobby) {
|
||||
if (l->is_game()) {
|
||||
games_total++;
|
||||
} else {
|
||||
lobbies_total++;
|
||||
}
|
||||
|
||||
for (size_t z = 0; z < l->max_clients; z++) {
|
||||
auto lc = l->clients[z];
|
||||
if (!lc) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const char* v = version_label(lc->version());
|
||||
if (l->is_game()) {
|
||||
players_in_games_total++;
|
||||
game_players_by_version[v]++;
|
||||
} else {
|
||||
players_in_lobbies_total++;
|
||||
lobby_players_by_version[v]++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
string server_name = escape_label(this->state->name);
|
||||
string revision = escape_label(GIT_REVISION_HASH);
|
||||
|
||||
string out;
|
||||
out += "# HELP pso_newserv_up Whether this newserv HTTP metrics endpoint is reachable\n";
|
||||
out += "# TYPE pso_newserv_up gauge\n";
|
||||
add_metric(out, "pso_newserv_up", 1);
|
||||
|
||||
out += "# HELP pso_newserv_build_info Build and server identity info\n";
|
||||
out += "# TYPE pso_newserv_build_info gauge\n";
|
||||
out += "pso_newserv_build_info{server_name=\"";
|
||||
out += server_name;
|
||||
out += "\",revision=\"";
|
||||
out += revision;
|
||||
out += "\",build_time=\"";
|
||||
out += std::to_string(BUILD_TIMESTAMP);
|
||||
out += "\"} 1\n";
|
||||
|
||||
out += "# HELP pso_newserv_clients_connected_total Connected clients, including patch/proxy/menu states\n";
|
||||
out += "# TYPE pso_newserv_clients_connected_total gauge\n";
|
||||
add_metric(out, "pso_newserv_clients_connected_total", connected_total);
|
||||
|
||||
out += "# HELP pso_newserv_clients_connected Connected clients by coarse version family\n";
|
||||
out += "# TYPE pso_newserv_clients_connected gauge\n";
|
||||
for (const auto& [version, count] : connected_by_version) {
|
||||
add_metric_1label(out, "pso_newserv_clients_connected", "version", version, count);
|
||||
}
|
||||
|
||||
out += "# HELP pso_newserv_lobbies_total Non-game lobby count\n";
|
||||
out += "# TYPE pso_newserv_lobbies_total gauge\n";
|
||||
add_metric(out, "pso_newserv_lobbies_total", lobbies_total);
|
||||
|
||||
out += "# HELP pso_newserv_games_total Game room count\n";
|
||||
out += "# TYPE pso_newserv_games_total gauge\n";
|
||||
add_metric(out, "pso_newserv_games_total", games_total);
|
||||
|
||||
out += "# HELP pso_newserv_players_in_lobbies_total Players currently in non-game lobbies\n";
|
||||
out += "# TYPE pso_newserv_players_in_lobbies_total gauge\n";
|
||||
add_metric(out, "pso_newserv_players_in_lobbies_total", players_in_lobbies_total);
|
||||
|
||||
out += "# HELP pso_newserv_players_in_games_total Players currently in game rooms\n";
|
||||
out += "# TYPE pso_newserv_players_in_games_total gauge\n";
|
||||
add_metric(out, "pso_newserv_players_in_games_total", players_in_games_total);
|
||||
|
||||
out += "# HELP pso_newserv_players_in_lobbies Players currently in non-game lobbies by coarse version family\n";
|
||||
out += "# TYPE pso_newserv_players_in_lobbies gauge\n";
|
||||
for (const auto& [version, count] : lobby_players_by_version) {
|
||||
add_metric_1label(out, "pso_newserv_players_in_lobbies", "version", version, count);
|
||||
}
|
||||
|
||||
out += "# HELP pso_newserv_players_in_games Players currently in game rooms by coarse version family\n";
|
||||
out += "# TYPE pso_newserv_players_in_games gauge\n";
|
||||
for (const auto& [version, count] : game_players_by_version) {
|
||||
add_metric_1label(out, "pso_newserv_players_in_games", "version", version, count);
|
||||
}
|
||||
|
||||
co_return RouterRetT(RawResponse{
|
||||
.content_type = "text/plain; version=0.0.4; charset=utf-8",
|
||||
.data = std::move(out),
|
||||
});
|
||||
});
|
||||
|
||||
this->router.add(HTTPRequest::Method::GET, "/y/clients", [this](ArgsT&&) -> RetT {
|
||||
auto res = make_shared<phosg::JSON>(phosg::JSON::list());
|
||||
for (const auto& c : this->state->game_server->all_clients()) {
|
||||
|
||||
+111
-93
@@ -38,6 +38,7 @@ ItemCreator::ItemCreator(
|
||||
shared_ptr<const BattleRules> restrictions)
|
||||
: log(std::format("[ItemCreator:{}/{}/{}/{}] ", phosg::name_for_enum(stack_limits->version), abbreviation_for_mode(mode), abbreviation_for_difficulty(difficulty), section_id), lobby_log.min_level),
|
||||
logic_version(stack_limits->version),
|
||||
is_legacy_replay(false),
|
||||
stack_limits(stack_limits),
|
||||
mode(mode),
|
||||
difficulty(difficulty),
|
||||
@@ -136,13 +137,13 @@ uint8_t ItemCreator::table_index_for_area(uint8_t area) const {
|
||||
return data[area];
|
||||
}
|
||||
|
||||
ItemCreator::DropResult ItemCreator::on_box_item_drop(uint8_t area) {
|
||||
ItemCreator::DropResult ItemCreator::on_box_item_drop(uint8_t area, bool force_rare) {
|
||||
try {
|
||||
uint8_t table_index = this->table_index_for_area(area);
|
||||
this->log.info_f("Box drop checks for area {:02X} (table index {:02X})", area, table_index);
|
||||
|
||||
DropResult res;
|
||||
res.item = this->check_rare_specs_and_create_rare_box_item(area);
|
||||
res.item = this->check_rare_specs_and_create_rare_box_item(area, force_rare);
|
||||
if (!res.item.empty()) {
|
||||
res.is_from_rare_table = true;
|
||||
} else {
|
||||
@@ -188,34 +189,40 @@ ItemCreator::DropResult ItemCreator::on_box_item_drop(uint8_t area) {
|
||||
}
|
||||
}
|
||||
|
||||
ItemCreator::DropResult ItemCreator::on_monster_item_drop(uint32_t enemy_type, uint8_t area) {
|
||||
ItemCreator::DropResult ItemCreator::on_monster_item_drop(EnemyType enemy_type, uint8_t area, bool force_rare) {
|
||||
try {
|
||||
// Note: The original GC implementation uses (enemy_type > 0x58) here; we extend it to the full array size for BB
|
||||
if (enemy_type >= NUM_RT_INDEXES_V4) {
|
||||
this->log.warning_f("Invalid enemy type: {:X}", enemy_type);
|
||||
return DropResult();
|
||||
}
|
||||
this->log.info_f("Enemy type: {:X}", enemy_type);
|
||||
// Note: The original implementation has a bounds check for enemy_type here, because it uses rt_index instead
|
||||
// if (enemy_type >= NUM_RT_INDEXES_V4) {
|
||||
// this->log.warning_f("Invalid enemy type: {:X}", enemy_type);
|
||||
// return DropResult();
|
||||
// }
|
||||
this->log.info_f("Enemy type: {}", phosg::name_for_enum(enemy_type));
|
||||
|
||||
auto pt = this->pt(area);
|
||||
uint8_t type_drop_prob = pt->enemy_type_drop_probs.at(enemy_type);
|
||||
uint8_t drop_sample = this->rand_int(100);
|
||||
if (drop_sample >= type_drop_prob) {
|
||||
this->log.info_f("Drop not chosen ({} >= {})", drop_sample, type_drop_prob);
|
||||
uint8_t type_drop_prob = 0;
|
||||
try {
|
||||
type_drop_prob = pt->enemy_type_drop_probs.at(enemy_type);
|
||||
} catch (const std::out_of_range&) {
|
||||
this->log.info_f("No drop probability is set for this enemy type");
|
||||
return DropResult();
|
||||
} else {
|
||||
this->log.info_f("Drop chosen ({} < {})", drop_sample, type_drop_prob);
|
||||
}
|
||||
if (!force_rare) {
|
||||
uint8_t drop_sample = this->rand_int(100);
|
||||
if (drop_sample >= type_drop_prob) {
|
||||
this->log.info_f("Drop not chosen ({} >= {})", drop_sample, type_drop_prob);
|
||||
return DropResult();
|
||||
} else {
|
||||
this->log.info_f("Drop chosen ({} < {})", drop_sample, type_drop_prob);
|
||||
}
|
||||
}
|
||||
|
||||
DropResult res;
|
||||
res.item = this->check_rare_spec_and_create_rare_enemy_item(enemy_type, area);
|
||||
res.item = this->check_rare_spec_and_create_rare_enemy_item(enemy_type, area, force_rare);
|
||||
if (!res.item.empty()) {
|
||||
res.is_from_rare_table = true;
|
||||
} else {
|
||||
uint32_t item_class_determinant =
|
||||
this->should_allow_meseta_drops() ? this->rand_int(3) : (this->rand_int(2) + 1);
|
||||
|
||||
uint32_t item_class;
|
||||
uint8_t item_class_determinant = this->should_allow_meseta_drops() ? this->rand_int(3) : (this->rand_int(2) + 1);
|
||||
uint8_t item_class;
|
||||
switch (item_class_determinant) {
|
||||
case 0:
|
||||
item_class = 5;
|
||||
@@ -224,7 +231,12 @@ ItemCreator::DropResult ItemCreator::on_monster_item_drop(uint32_t enemy_type, u
|
||||
item_class = 4;
|
||||
break;
|
||||
case 2:
|
||||
item_class = pt->enemy_item_classes.at(enemy_type);
|
||||
try {
|
||||
item_class = pt->enemy_type_item_classes.at(enemy_type);
|
||||
} catch (const out_of_range&) {
|
||||
this->log.info_f("Item class is not set for this enemy type");
|
||||
item_class = 0xFF;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw logic_error("invalid item class determinant");
|
||||
@@ -251,7 +263,12 @@ ItemCreator::DropResult ItemCreator::on_monster_item_drop(uint32_t enemy_type, u
|
||||
break;
|
||||
case 5: // Meseta
|
||||
res.item.data1[0] = 0x04;
|
||||
res.item.data2d = this->choose_meseta_amount(pt->enemy_meseta_ranges, enemy_type) & 0xFFFF;
|
||||
try {
|
||||
res.item.data2d = this->choose_meseta_amount(pt->enemy_type_meseta_ranges.at(enemy_type)) & 0xFFFF;
|
||||
} catch (const out_of_range&) {
|
||||
this->log.info_f("Meseta range is not set for this enemy type");
|
||||
return DropResult();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return res;
|
||||
@@ -270,30 +287,56 @@ ItemCreator::DropResult ItemCreator::on_monster_item_drop(uint32_t enemy_type, u
|
||||
}
|
||||
}
|
||||
|
||||
ItemData ItemCreator::check_rare_specs_and_create_rare_box_item(uint8_t area) {
|
||||
ItemData item;
|
||||
ItemData ItemCreator::check_rare_specs_and_create_rare_item(
|
||||
const std::vector<RareItemSet::ExpandedDrop>& specs, uint8_t area, bool force_rare) {
|
||||
if (specs.empty()) {
|
||||
return ItemData();
|
||||
}
|
||||
|
||||
// This logic differs from the original client logic. This logic "stacks" all rare rates into a single probability
|
||||
// space, whereas the original client logic chooses a new random number for each rare spec that it checks. The
|
||||
// stacking logic makes the order of specs irrelevant, whereas the original client logic means that later specs are
|
||||
// actually more rare than they should be. In the original client, this only matters for boxes, because enemies could
|
||||
// not have multiple specs. Also, the original code uses 0xFFFFFFFF as the maximum here; we use 0x100000000 instead,
|
||||
// which makes all rare items SLIGHTLY more rare.
|
||||
int64_t det = force_rare ? 0 : this->rand_int(0x100000000);
|
||||
if (this->is_legacy_replay) {
|
||||
// For some old tests, we waste a few replay values because they used the old (non-stacked) logic. New tests should
|
||||
// not use this codepath.
|
||||
for (size_t z = 1; z < specs.size(); z++) {
|
||||
this->rand_int(0x100000000);
|
||||
}
|
||||
}
|
||||
this->log.info_f("{} specs to check with det={:08X} rare_mult={:g}", specs.size(), det, this->rare_drop_rate_multiplier);
|
||||
for (const auto& spec : specs) {
|
||||
uint64_t effective_probability = spec.probability;
|
||||
if (this->rare_drop_rate_multiplier != 1.0) {
|
||||
double multiplied_probability = static_cast<double>(spec.probability) * this->rare_drop_rate_multiplier;
|
||||
effective_probability = (multiplied_probability >= 4294967296.0)
|
||||
? 0x100000000ULL
|
||||
: static_cast<uint64_t>(multiplied_probability);
|
||||
}
|
||||
if (this->log.should_log(phosg::LogLevel::L_INFO)) {
|
||||
this->log.info_f("Checking spec {:08X} => effective {:08X} => {} with det={:08X}",
|
||||
spec.probability, effective_probability, spec.data.hex(), det);
|
||||
}
|
||||
det -= effective_probability;
|
||||
if (det < 0) {
|
||||
return this->create_rare_item(spec.data, area);
|
||||
}
|
||||
}
|
||||
return ItemData();
|
||||
}
|
||||
|
||||
ItemData ItemCreator::check_rare_specs_and_create_rare_box_item(uint8_t area, bool force_rare) {
|
||||
if (!this->are_rare_drops_allowed()) {
|
||||
return item;
|
||||
return ItemData();
|
||||
}
|
||||
|
||||
uint8_t table_index = this->table_index_for_area(area);
|
||||
Episode episode = episode_for_area(area);
|
||||
auto rare_specs = this->rare_item_set->get_box_specs(this->mode, episode, this->difficulty, this->section_id, table_index);
|
||||
for (const auto& spec : rare_specs) {
|
||||
item = this->check_rate_and_create_rare_item(spec, area);
|
||||
if (!item.empty()) {
|
||||
if (this->log.should_log(phosg::LogLevel::L_INFO)) {
|
||||
auto hex = spec.data.hex();
|
||||
this->log.info_f("Box spec {:08X} produced item {}", spec.probability, hex);
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (this->log.should_log(phosg::LogLevel::L_INFO)) {
|
||||
auto hex = spec.data.hex();
|
||||
this->log.info_f("Box spec {:08X} did not produce item {}", spec.probability, hex);
|
||||
}
|
||||
}
|
||||
return item;
|
||||
auto specs = this->rare_item_set->get_box_specs(this->mode, episode, this->difficulty, this->section_id, table_index);
|
||||
return this->check_rare_specs_and_create_rare_item(specs, area, force_rare);
|
||||
}
|
||||
|
||||
uint32_t ItemCreator::rand_int(uint64_t max) {
|
||||
@@ -305,24 +348,18 @@ float ItemCreator::rand_float_0_1_from_crypt() {
|
||||
return (static_cast<double>(this->rand_crypt->next() >> 16) / 65536.0);
|
||||
}
|
||||
|
||||
template <size_t NumRanges>
|
||||
uint32_t ItemCreator::choose_meseta_amount(
|
||||
const parray<CommonItemSet::Table::Range<uint16_t>, NumRanges> ranges,
|
||||
size_t table_index) {
|
||||
uint16_t min = ranges[table_index].min;
|
||||
uint16_t max = ranges[table_index].max;
|
||||
|
||||
uint32_t ItemCreator::choose_meseta_amount(const CommonItemSet::Table::Range<uint16_t>& range) {
|
||||
// Note: The original code returns 0xFF here if either limit is equal to 0xFF (despite them being 16-bit integers!)
|
||||
uint16_t ret;
|
||||
if (min == max) {
|
||||
ret = min;
|
||||
} else if (max < min) {
|
||||
ret = this->rand_int((min - max) + 1) + max;
|
||||
if (range.min == range.max) {
|
||||
ret = range.min;
|
||||
} else if (range.max < range.min) {
|
||||
ret = this->rand_int((range.min - range.max) + 1) + range.max;
|
||||
} else {
|
||||
ret = this->rand_int((max - min) + 1) + min;
|
||||
ret = this->rand_int((range.max - range.min) + 1) + range.min;
|
||||
}
|
||||
|
||||
this->log.info_f("Chose {} Meseta from range [{}, {}]", ret, min, max);
|
||||
this->log.info_f("Chose {} Meseta from range [{}, {}]", ret, range.min, range.max);
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -330,45 +367,24 @@ bool ItemCreator::should_allow_meseta_drops() const {
|
||||
return (this->mode != GameMode::CHALLENGE);
|
||||
}
|
||||
|
||||
ItemData ItemCreator::check_rare_spec_and_create_rare_enemy_item(uint32_t enemy_type, uint8_t area) {
|
||||
ItemData item;
|
||||
if (this->are_rare_drops_allowed() && (enemy_type > 0) && (enemy_type < NUM_RT_INDEXES_V4)) {
|
||||
// Note: In the original implementation, enemies can only have one possible rare drop. In our implementation, they
|
||||
// can have multiple rare drops if JSONRareItemSet is used (the other RareItemSet implementations never return
|
||||
// multiple drops for an enemy type).
|
||||
Episode episode = episode_for_area(area);
|
||||
auto rare_specs = this->rare_item_set->get_enemy_specs(
|
||||
this->mode, episode, this->difficulty, this->section_id, enemy_type);
|
||||
for (const auto& spec : rare_specs) {
|
||||
item = this->check_rate_and_create_rare_item(spec, area);
|
||||
if (!item.empty()) {
|
||||
if (this->log.should_log(phosg::LogLevel::L_INFO)) {
|
||||
auto hex = spec.data.hex();
|
||||
this->log.info_f("Enemy spec {:08X} produced item {}", spec.probability, hex);
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (this->log.should_log(phosg::LogLevel::L_INFO)) {
|
||||
auto hex = spec.data.hex();
|
||||
this->log.info_f("Enemy spec {:08X} did not produce item {}", spec.probability, hex);
|
||||
}
|
||||
}
|
||||
ItemData ItemCreator::check_rare_spec_and_create_rare_enemy_item(EnemyType enemy_type, uint8_t area, bool force_rare) {
|
||||
// Note: The original implementation has a bounds check for enemy_type here, since it uses rt_index instead.
|
||||
// if ((enemy_type <= 0) || (enemy_type >= NUM_RT_INDEXES_V4)) return ItemData{};
|
||||
if (!this->are_rare_drops_allowed()) {
|
||||
return ItemData{};
|
||||
}
|
||||
return item;
|
||||
|
||||
// Note: In the original implementation, enemies can only have one possible rare drop. In our implementation, they
|
||||
// can have multiple rare drops if JSONRareItemSet is used (the other RareItemSet implementations never return
|
||||
// multiple drops for an enemy type).
|
||||
Episode episode = episode_for_area(area);
|
||||
auto specs = this->rare_item_set->get_enemy_specs(
|
||||
this->mode, episode, this->difficulty, this->section_id, enemy_type);
|
||||
return this->check_rare_specs_and_create_rare_item(specs, area, force_rare);
|
||||
}
|
||||
|
||||
ItemData ItemCreator::check_rate_and_create_rare_item(const RareItemSet::ExpandedDrop& drop, uint8_t area) {
|
||||
if (drop.probability == 0) {
|
||||
return ItemData();
|
||||
}
|
||||
|
||||
// Note: The original code uses 0xFFFFFFFF as the maximum here. We use 0x100000000 instead, which makes all rare
|
||||
// items SLIGHTLY more rare.
|
||||
if (this->rand_int(0x100000000) >= drop.probability) {
|
||||
return ItemData();
|
||||
}
|
||||
|
||||
ItemData item = drop.data;
|
||||
ItemData ItemCreator::create_rare_item(const ItemData& drop_item, uint8_t area) {
|
||||
ItemData item = drop_item;
|
||||
if (item.can_be_encoded_in_rel_rare_table()) {
|
||||
switch (item.data1[0]) {
|
||||
case 0:
|
||||
@@ -617,9 +633,11 @@ void ItemCreator::generate_common_item_variances(ItemData& item, uint8_t area) {
|
||||
case 3:
|
||||
this->generate_common_tool_variances(item, area);
|
||||
break;
|
||||
case 4:
|
||||
item.data2d = this->choose_meseta_amount(this->pt(area)->box_meseta_ranges, this->table_index_for_area(area)) & 0xFFFF;
|
||||
case 4: {
|
||||
const auto& range = this->pt(area)->box_meseta_ranges.at(this->table_index_for_area(area));
|
||||
item.data2d = this->choose_meseta_amount(range) & 0xFFFF;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
// Note: The original code does the following here:
|
||||
// item.clear();
|
||||
@@ -817,7 +835,7 @@ uint8_t ItemCreator::choose_weapon_special(uint8_t det) {
|
||||
uint8_t det2 = this->rand_int(maxes[det]);
|
||||
this->log.info_f("Choosing special with det {:02X} and det2 {:02X}", det, det2);
|
||||
size_t index = 0;
|
||||
for (size_t z = 1; z < this->item_parameter_table->num_specials; z++) {
|
||||
for (size_t z = 1; z < this->item_parameter_table->num_specials(); z++) {
|
||||
if (det + 1 == this->item_parameter_table->get_special_stars(z)) {
|
||||
if (index == det2) {
|
||||
this->log.info_f("Chose special {:02X}", z);
|
||||
|
||||
+19
-8
@@ -37,10 +37,18 @@ public:
|
||||
bool is_from_rare_table = false;
|
||||
};
|
||||
|
||||
DropResult on_monster_item_drop(uint32_t enemy_type, uint8_t area);
|
||||
DropResult on_box_item_drop(uint8_t area);
|
||||
inline void set_legacy_replay() {
|
||||
this->is_legacy_replay = true;
|
||||
}
|
||||
inline void set_rare_drop_rate_multiplier(double multiplier) {
|
||||
this->rare_drop_rate_multiplier = multiplier;
|
||||
}
|
||||
|
||||
DropResult on_monster_item_drop(EnemyType enemy_type, uint8_t area, bool force_rare);
|
||||
DropResult on_box_item_drop(uint8_t area, bool force_rare);
|
||||
// Note: param3-6 refer to the corresponding fields of the object definition
|
||||
DropResult on_specialized_box_item_drop(uint8_t area, float param3, uint32_t param4, uint32_t param5, uint32_t param6);
|
||||
DropResult on_specialized_box_item_drop(
|
||||
uint8_t area, float param3, uint32_t param4, uint32_t param5, uint32_t param6);
|
||||
ItemData base_item_for_specialized_box(uint32_t param4, uint32_t param5, uint32_t param6) const;
|
||||
|
||||
std::vector<ItemData> generate_armor_shop_contents(Episode episode, size_t player_level);
|
||||
@@ -69,6 +77,8 @@ private:
|
||||
|
||||
phosg::PrefixedLogger log;
|
||||
Version logic_version;
|
||||
bool is_legacy_replay;
|
||||
double rare_drop_rate_multiplier = 1.0;
|
||||
std::shared_ptr<const ItemData::StackLimits> stack_limits;
|
||||
GameMode mode;
|
||||
Difficulty difficulty;
|
||||
@@ -116,14 +126,15 @@ private:
|
||||
uint32_t rand_int(uint64_t max);
|
||||
float rand_float_0_1_from_crypt();
|
||||
|
||||
template <size_t NumRanges>
|
||||
uint32_t choose_meseta_amount(const parray<CommonItemSet::Table::Range<uint16_t>, NumRanges> ranges, size_t table_index);
|
||||
uint32_t choose_meseta_amount(const CommonItemSet::Table::Range<uint16_t>& range);
|
||||
|
||||
bool should_allow_meseta_drops() const;
|
||||
|
||||
ItemData check_rare_spec_and_create_rare_enemy_item(uint32_t enemy_type, uint8_t area);
|
||||
ItemData check_rare_specs_and_create_rare_box_item(uint8_t area);
|
||||
ItemData check_rate_and_create_rare_item(const RareItemSet::ExpandedDrop& drop, uint8_t area);
|
||||
ItemData check_rare_spec_and_create_rare_enemy_item(EnemyType enemy_type, uint8_t area, bool force_rare);
|
||||
ItemData check_rare_specs_and_create_rare_box_item(uint8_t area, bool force_rare);
|
||||
ItemData check_rare_specs_and_create_rare_item(
|
||||
const std::vector<RareItemSet::ExpandedDrop>& specs, uint8_t area, bool force_rare);
|
||||
ItemData create_rare_item(const ItemData& drop_item, uint8_t area);
|
||||
|
||||
void generate_rare_weapon_bonuses(ItemData& item, Episode episode, uint32_t random_sample);
|
||||
void deduplicate_weapon_bonuses(ItemData& item) const;
|
||||
|
||||
+1
-1
@@ -486,7 +486,7 @@ void ItemData::encode_for_version(Version to_version, shared_ptr<const ItemParam
|
||||
if (should_encode_v2_data && (this->data1[1] > 0x26)) {
|
||||
if (this->data1[1] < 0x89) {
|
||||
this->data1[5] = this->data1[1];
|
||||
this->data1[1] = item_parameter_table->get_weapon_v1_replacement(this->data1[1]);
|
||||
this->data1[1] = item_parameter_table->get_weapon_kind(this->data1[1]);
|
||||
if (this->data1[1] == 0x00) {
|
||||
this->data1[1] = 0x0F;
|
||||
}
|
||||
|
||||
+230
-46
@@ -1,5 +1,7 @@
|
||||
#include "ItemNameIndex.hh"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "StaticGameData.hh"
|
||||
|
||||
using namespace std;
|
||||
@@ -787,9 +789,9 @@ void ItemNameIndex::print_table(FILE* stream) const {
|
||||
auto pmt = this->item_parameter_table;
|
||||
|
||||
phosg::fwrite_fmt(stream, "WEAPONS\n");
|
||||
phosg::fwrite_fmt(stream, " CODE => ---ID--- TYPE SKIN POINTS FLAG ATPLO ATPHI ATPRQ MSTRQ ATARQ -MST- GND PH SP ATA SB(S1:AMT1,S2:AMT2) PJ 1X 1Y 2X 2Y CL --A1-- A4 A5 TB BF V1 ST* USL ---DIVISOR--- NAME\n");
|
||||
for (size_t data1_1 = 0; data1_1 < pmt->num_weapon_classes; data1_1++) {
|
||||
uint8_t v1_replacement = pmt->get_weapon_v1_replacement(data1_1);
|
||||
phosg::fwrite_fmt(stream, " CODE => ---ID--- TYPE SKIN POINTS FLAG ATPLO ATPHI ATPRQ MSTRQ ATARQ -MST- GND PH SP ATA SB(S1:AMT1,S2:AMT2) PJ 1X 1Y 2X 2Y CR --A1-- A4 A5 TB(TN:FL:AMOUNT, ... ) BF CL ST* USL ---DIVISOR--- NAME\n");
|
||||
for (size_t data1_1 = 0; data1_1 < pmt->num_weapon_classes(); data1_1++) {
|
||||
uint8_t weapon_class = pmt->get_weapon_kind(data1_1);
|
||||
float sale_divisor = pmt->get_sale_divisor(0x00, data1_1);
|
||||
string divisor_str = std::format("{:g}", sale_divisor);
|
||||
divisor_str.resize(13, ' ');
|
||||
@@ -797,7 +799,7 @@ void ItemNameIndex::print_table(FILE* stream) const {
|
||||
size_t data1_2_limit = pmt->num_weapons_in_class(data1_1);
|
||||
for (size_t data1_2 = 0; data1_2 < data1_2_limit; data1_2++) {
|
||||
const auto& w = pmt->get_weapon(data1_1, data1_2);
|
||||
uint8_t stars = pmt->get_item_stars(w.base.id);
|
||||
uint8_t stars = pmt->get_item_stars(w.id);
|
||||
bool is_unsealable = pmt->is_unsealable_item(0x00, data1_1, data1_2);
|
||||
|
||||
ItemData item;
|
||||
@@ -806,14 +808,30 @@ void ItemNameIndex::print_table(FILE* stream) const {
|
||||
item.data1[2] = data1_2;
|
||||
string name = this->describe_item(item);
|
||||
|
||||
auto& stat_boost = pmt->get_stat_boost(w.stat_boost_entry_index);
|
||||
phosg::fwrite_fmt(stream, " 00{:02X}{:02X} => {:08X} {:04X} {:04X} {:6} {:04X} {:5} {:5} {:5} {:5} {:5} {:5} {:3} {:02X} {:02X} {:3} {:02X}({:02X}:{:04X},{:02X}:{:04X}) {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X}{:02X}{:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:2}* {} {} {}\n",
|
||||
const auto& stat_boost = pmt->get_stat_boost(w.stat_boost_entry_index);
|
||||
|
||||
string tech_boost_str;
|
||||
if (w.tech_boost_entry_index < pmt->num_tech_boosts()) {
|
||||
const auto& tech_boost = pmt->get_tech_boost(w.tech_boost_entry_index);
|
||||
tech_boost_str = std::format("({:02X}:{:02X}:{:g},{:02X}:{:02X}:{:g},{:02X}:{:02X}:{:g})",
|
||||
tech_boost.tech_num1, tech_boost.flags1, tech_boost.amount1, tech_boost.tech_num2, tech_boost.flags2,
|
||||
tech_boost.amount2, tech_boost.tech_num3, tech_boost.flags3, tech_boost.amount3);
|
||||
}
|
||||
tech_boost_str.resize(40, ' ');
|
||||
|
||||
phosg::fwrite_fmt(stream,
|
||||
// CODE => ID TYPE SKIN PTS FLAG ATP- ATP+ ATPR MSTR ATAR MST GND PH SP ATA
|
||||
" 00{:02X}{:02X} => {:08X} {:04X} {:04X} {:6} {:04X} {:5} {:5} {:5} {:5} {:5} {:5} {:3} {:02X} {:02X} {:3} "
|
||||
// SB( S1:AMT1 , S2:AMT2 ) PJ 1X 1Y 2X 2Y CR --------A1-------- A4
|
||||
"{:02X}({:02X}:{:04X},{:02X}:{:04X}) {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X}{:02X}{:02X} {:02X} "
|
||||
// A5 TB( TN: FL: AMT, TN: FL: AMT, TN: FL: AMT) BF CL ST* US DV NAME\n"
|
||||
"{:02X} {:02X}{} {:02X} {:02X} {:2}* {} {} {}\n",
|
||||
data1_1,
|
||||
data1_2,
|
||||
w.base.id,
|
||||
w.base.type,
|
||||
w.base.skin,
|
||||
w.base.team_points,
|
||||
w.id,
|
||||
w.type,
|
||||
w.skin,
|
||||
w.team_points,
|
||||
w.class_flags,
|
||||
w.atp_min,
|
||||
w.atp_max,
|
||||
@@ -826,10 +844,10 @@ void ItemNameIndex::print_table(FILE* stream) const {
|
||||
w.special,
|
||||
w.ata,
|
||||
w.stat_boost_entry_index,
|
||||
stat_boost.stats[0],
|
||||
stat_boost.amounts[0],
|
||||
stat_boost.stats[1],
|
||||
stat_boost.amounts[1],
|
||||
stat_boost.stat1,
|
||||
stat_boost.amount1,
|
||||
stat_boost.stat2,
|
||||
stat_boost.amount2,
|
||||
w.projectile,
|
||||
w.trail1_x,
|
||||
w.trail1_y,
|
||||
@@ -841,9 +859,10 @@ void ItemNameIndex::print_table(FILE* stream) const {
|
||||
w.unknown_a1[2],
|
||||
w.unknown_a4,
|
||||
w.unknown_a5,
|
||||
w.tech_boost,
|
||||
w.tech_boost_entry_index,
|
||||
tech_boost_str,
|
||||
w.behavior_flags,
|
||||
v1_replacement,
|
||||
weapon_class,
|
||||
stars,
|
||||
is_unsealable ? "YES" : " no",
|
||||
divisor_str,
|
||||
@@ -852,7 +871,7 @@ void ItemNameIndex::print_table(FILE* stream) const {
|
||||
}
|
||||
|
||||
phosg::fwrite_fmt(stream, "ARMORS\n");
|
||||
phosg::fwrite_fmt(stream, " CODE => ---ID--- TYPE SKIN POINTS -DFP- -EVP- BP BE FLAG LVL EFR ETH EIC EDK ELT DFR EVR SB(S1:AMT1,S2:AMT2) TB FT A4 ST* ---DIVISOR--- NAME\n");
|
||||
phosg::fwrite_fmt(stream, " CODE => ---ID--- TYPE SKIN POINTS -DFP- -EVP- BP BE FLAG LVL EFR ETH EIC EDK ELT DFR EVR SB(S1:AMT1,S2:AMT2) TB(TN:FL:AMOUNT, ... ) FT A4 ST* ---DIVISOR--- NAME\n");
|
||||
for (size_t data1_1 = 1; data1_1 < 3; data1_1++) {
|
||||
float sale_divisor = pmt->get_sale_divisor(0x01, data1_1);
|
||||
string divisor_str = std::format("{:g}", sale_divisor);
|
||||
@@ -861,7 +880,7 @@ void ItemNameIndex::print_table(FILE* stream) const {
|
||||
size_t data1_2_limit = pmt->num_armors_or_shields_in_class(data1_1);
|
||||
for (size_t data1_2 = 0; data1_2 < data1_2_limit; data1_2++) {
|
||||
const auto& a = pmt->get_armor_or_shield(data1_1, data1_2);
|
||||
uint8_t stars = pmt->get_item_stars(a.base.id);
|
||||
uint8_t stars = pmt->get_item_stars(a.id);
|
||||
|
||||
ItemData item;
|
||||
item.data1[0] = 0x01;
|
||||
@@ -870,13 +889,23 @@ void ItemNameIndex::print_table(FILE* stream) const {
|
||||
string name = this->describe_item(item);
|
||||
|
||||
auto& stat_boost = pmt->get_stat_boost(a.stat_boost_entry_index);
|
||||
phosg::fwrite_fmt(stream, " 01{:02X}{:02X} => {:08X} {:04X} {:04X} {:6} {:5} {:5} {:02X} {:02X} {:04X} {:3} {:3} {:3} {:3} {:3} {:3} {:3} {:3} {:02X}({:02X}:{:04X},{:02X}:{:04X}) {:02X} {:02X} {:02X} {:2}* {} {}\n",
|
||||
|
||||
string tech_boost_str;
|
||||
if (a.tech_boost_entry_index < pmt->num_tech_boosts()) {
|
||||
const auto& tech_boost = pmt->get_tech_boost(a.tech_boost_entry_index);
|
||||
tech_boost_str = std::format("({:02X}:{:02X}:{:g},{:02X}:{:02X}:{:g},{:02X}:{:02X}:{:g})",
|
||||
tech_boost.tech_num1, tech_boost.flags1, tech_boost.amount1, tech_boost.tech_num2, tech_boost.flags2,
|
||||
tech_boost.amount2, tech_boost.tech_num3, tech_boost.flags3, tech_boost.amount3);
|
||||
}
|
||||
tech_boost_str.resize(40, ' ');
|
||||
|
||||
phosg::fwrite_fmt(stream, " 01{:02X}{:02X} => {:08X} {:04X} {:04X} {:6} {:5} {:5} {:02X} {:02X} {:04X} {:3} {:3} {:3} {:3} {:3} {:3} {:3} {:3} {:02X}({:02X}:{:04X},{:02X}:{:04X}) {:02X}{} {:02X} {:02X} {:2}* {} {}\n",
|
||||
data1_1,
|
||||
data1_2,
|
||||
a.base.id,
|
||||
a.base.type,
|
||||
a.base.skin,
|
||||
a.base.team_points,
|
||||
a.id,
|
||||
a.type,
|
||||
a.skin,
|
||||
a.team_points,
|
||||
a.dfp,
|
||||
a.evp,
|
||||
a.block_particle,
|
||||
@@ -891,11 +920,12 @@ void ItemNameIndex::print_table(FILE* stream) const {
|
||||
a.dfp_range,
|
||||
a.evp_range,
|
||||
a.stat_boost_entry_index,
|
||||
stat_boost.stats[0],
|
||||
stat_boost.amounts[0],
|
||||
stat_boost.stats[1],
|
||||
stat_boost.amounts[1],
|
||||
a.tech_boost,
|
||||
stat_boost.stat1,
|
||||
stat_boost.amount1,
|
||||
stat_boost.stat2,
|
||||
stat_boost.amount2,
|
||||
a.tech_boost_entry_index,
|
||||
tech_boost_str,
|
||||
a.flags_type,
|
||||
a.unknown_a4,
|
||||
stars,
|
||||
@@ -914,7 +944,7 @@ void ItemNameIndex::print_table(FILE* stream) const {
|
||||
size_t data1_2_limit = pmt->num_units();
|
||||
for (size_t data1_2 = 0; data1_2 < data1_2_limit; data1_2++) {
|
||||
const auto& u = pmt->get_unit(data1_2);
|
||||
uint8_t stars = pmt->get_item_stars(u.base.id);
|
||||
uint8_t stars = pmt->get_item_stars(u.id);
|
||||
|
||||
ItemData item;
|
||||
item.data1[0] = 0x01;
|
||||
@@ -924,10 +954,10 @@ void ItemNameIndex::print_table(FILE* stream) const {
|
||||
|
||||
phosg::fwrite_fmt(stream, " 0103{:02X} => {:08X} {:04X} {:04X} {:6} {:04X} {:5} {:6} {:2}* {} {}\n",
|
||||
data1_2,
|
||||
u.base.id,
|
||||
u.base.type,
|
||||
u.base.skin,
|
||||
u.base.team_points,
|
||||
u.id,
|
||||
u.type,
|
||||
u.skin,
|
||||
u.team_points,
|
||||
u.stat,
|
||||
u.stat_amount,
|
||||
u.modifier_amount,
|
||||
@@ -956,10 +986,10 @@ void ItemNameIndex::print_table(FILE* stream) const {
|
||||
|
||||
phosg::fwrite_fmt(stream, " 02{:02X}00 => {:08X} {:04X} {:04X} {:6} {:04X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:04X} {} {}\n",
|
||||
data1_1,
|
||||
m.base.id,
|
||||
m.base.type,
|
||||
m.base.skin,
|
||||
m.base.team_points,
|
||||
m.id,
|
||||
m.type,
|
||||
m.skin,
|
||||
m.team_points,
|
||||
m.feed_table,
|
||||
m.photon_blast,
|
||||
m.activation,
|
||||
@@ -979,7 +1009,7 @@ void ItemNameIndex::print_table(FILE* stream) const {
|
||||
|
||||
phosg::fwrite_fmt(stream, "TOOLS\n");
|
||||
phosg::fwrite_fmt(stream, " CODE => ---ID--- TYPE SKIN POINTS COUNT TECH -COST- ITEMFLAG ---DIVISOR--- NAME\n");
|
||||
for (size_t data1_1 = 0; data1_1 < pmt->num_tool_classes; data1_1++) {
|
||||
for (size_t data1_1 = 0; data1_1 < pmt->num_tool_classes(); data1_1++) {
|
||||
float sale_divisor = pmt->get_sale_divisor(0x03, data1_1);
|
||||
string divisor_str = std::format("{:g}", sale_divisor);
|
||||
divisor_str.resize(13, ' ');
|
||||
@@ -998,10 +1028,10 @@ void ItemNameIndex::print_table(FILE* stream) const {
|
||||
phosg::fwrite_fmt(stream, " 03{:02X}{:02X} => {:08X} {:04X} {:04X} {:6} {:5} {:04X} {:6} {:08X} {} {}\n",
|
||||
data1_1,
|
||||
data1_2,
|
||||
t.base.id,
|
||||
t.base.type,
|
||||
t.base.skin,
|
||||
t.base.team_points,
|
||||
t.id,
|
||||
t.type,
|
||||
t.skin,
|
||||
t.team_points,
|
||||
t.amount,
|
||||
t.tech,
|
||||
t.cost,
|
||||
@@ -1040,8 +1070,8 @@ void ItemNameIndex::print_table(FILE* stream) const {
|
||||
}
|
||||
|
||||
phosg::fwrite_fmt(stream, "SPECIAL DEFINITIONS\n");
|
||||
phosg::fwrite_fmt(stream, " SPECIAL => TYPE COUNT ST* NAME\n");
|
||||
for (size_t index = 0; index < pmt->num_specials; index++) {
|
||||
phosg::fwrite_fmt(stream, " SP => TYPE COUNT ST* NAME\n");
|
||||
for (size_t index = 0; index < pmt->num_specials(); index++) {
|
||||
const auto& sp = pmt->get_special(index);
|
||||
uint8_t stars = pmt->get_special_stars(index);
|
||||
const char* name = "";
|
||||
@@ -1051,12 +1081,12 @@ void ItemNameIndex::print_table(FILE* stream) const {
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
}
|
||||
phosg::fwrite_fmt(stream, " {:02X} => {:04X} {:5} {:2}* {}\n", index, sp.type, sp.amount, stars, name);
|
||||
phosg::fwrite_fmt(stream, " {:02X} => {:04X} {:5} {:2}* {}\n", index, sp.type, sp.amount, stars, name);
|
||||
}
|
||||
|
||||
phosg::fwrite_fmt(stream, "ITEM COMBINATIONS\n");
|
||||
phosg::fwrite_fmt(stream, " ---USE + -EQUIP => RESULT MLV GND LVL CLS\n");
|
||||
for (const auto& combo_list_it : pmt->get_all_item_combinations()) {
|
||||
for (const auto& combo_list_it : pmt->item_combinations_index()) {
|
||||
for (const auto& combo : combo_list_it.second) {
|
||||
phosg::fwrite_fmt(stream, " {:02X}{:02X}{:02X} + {:02X}{:02X}{:02X} => {:02X}{:02X}{:02X}",
|
||||
combo.used_item[0], combo.used_item[1], combo.used_item[2],
|
||||
@@ -1096,4 +1126,158 @@ void ItemNameIndex::print_table(FILE* stream) const {
|
||||
event_item.item[0], event_item.item[1], event_item.item[2], event_item.probability);
|
||||
}
|
||||
}
|
||||
|
||||
phosg::fwrite_fmt(stream, "PHOTON COLORS\n");
|
||||
phosg::fwrite_fmt(stream, " ## => ---A1--- (A2) (A3)\n");
|
||||
for (size_t z = 0; z < pmt->num_photon_colors(); z++) {
|
||||
const auto& pc = pmt->get_photon_color(z);
|
||||
phosg::fwrite_fmt(stream, " {:02X} => {:08X} ({:g}, {:g}, {:g}, {:g}) ({:g}, {:g}, {:g}, {:g})\n",
|
||||
z, pc.unknown_a1, pc.unknown_a2.x, pc.unknown_a2.y, pc.unknown_a2.z, pc.unknown_a2.t,
|
||||
pc.unknown_a3.x, pc.unknown_a3.y, pc.unknown_a3.z, pc.unknown_a3.t);
|
||||
}
|
||||
|
||||
phosg::fwrite_fmt(stream, "WEAPON RANGES\n");
|
||||
phosg::fwrite_fmt(stream, " ## => ---A3--- ---A4--- ---A5--- (A1) (A2)\n");
|
||||
for (size_t z = 0; z < pmt->num_weapon_ranges(); z++) {
|
||||
const auto& wr = pmt->get_weapon_range(z);
|
||||
phosg::fwrite_fmt(stream, " {:02X} => {:08X} {:08X} {:08X} ({:g}) ({:g})\n",
|
||||
z, wr.unknown_a3, wr.unknown_a4, wr.unknown_a5, wr.unknown_a1, wr.unknown_a2);
|
||||
}
|
||||
|
||||
phosg::fwrite_fmt(stream, "SALE DIVISORS\n");
|
||||
phosg::fwrite_fmt(stream, " ARMOR = {:g}\n", pmt->get_sale_divisor(1, 1));
|
||||
phosg::fwrite_fmt(stream, " SHIELD = {:g}\n", pmt->get_sale_divisor(1, 2));
|
||||
phosg::fwrite_fmt(stream, " UNIT = {:g}\n", pmt->get_sale_divisor(1, 3));
|
||||
phosg::fwrite_fmt(stream, " MAG = {:g}\n", pmt->get_sale_divisor(2, 0));
|
||||
|
||||
auto write_data_string = [&](const std::string& data, size_t addr = 0) -> void {
|
||||
if (data.empty()) {
|
||||
phosg::fwrite_fmt(stream, " (no data)\n");
|
||||
} else {
|
||||
auto data_str = phosg::format_data(data, addr);
|
||||
phosg::strip_trailing_whitespace(data_str);
|
||||
phosg::fwrite_fmt(stream, " {}\n", phosg::str_replace_all(data_str, "\n", "\n "));
|
||||
}
|
||||
};
|
||||
|
||||
phosg::fwrite_fmt(stream, "STAR VALUES\n");
|
||||
write_data_string(pmt->get_star_value_table(), pmt->get_star_value_index_range().first);
|
||||
|
||||
phosg::fwrite_fmt(stream, "UNKNOWN_A1\n");
|
||||
write_data_string(pmt->get_unknown_a1());
|
||||
|
||||
phosg::fwrite_fmt(stream, "WEAPON EFFECTS\n");
|
||||
phosg::fwrite_fmt(stream, " ## => -SOUND1- -VALUE1- -SOUND2- -VALUE2- ----------------A5---------------\n");
|
||||
for (size_t z = 0; z < pmt->num_weapon_effects(); z++) {
|
||||
const auto& we = pmt->get_weapon_effect(z);
|
||||
auto a5_str = phosg::format_data_string(we.unknown_a5.data(), we.unknown_a5.size());
|
||||
phosg::fwrite_fmt(stream, " {:02X} => {:08X} {:08X} {:08X} {:08X} {}\n",
|
||||
z, we.sound_id1, we.eff_value1, we.sound_id2, we.eff_value2, a5_str);
|
||||
}
|
||||
|
||||
phosg::fwrite_fmt(stream, "WEAPON STAT BOOST INDEX TABLE\n");
|
||||
write_data_string(pmt->get_weapon_stat_boost_index_table());
|
||||
|
||||
phosg::fwrite_fmt(stream, "ARMOR STAT BOOST INDEX TABLE\n");
|
||||
write_data_string(pmt->get_armor_stat_boost_index_table());
|
||||
|
||||
phosg::fwrite_fmt(stream, "SHIELD STAT BOOST INDEX TABLE\n");
|
||||
write_data_string(pmt->get_shield_stat_boost_index_table());
|
||||
|
||||
phosg::fwrite_fmt(stream, "STAT BOOSTS\n");
|
||||
phosg::fwrite_fmt(stream, " ## => BOOSTS\n");
|
||||
for (size_t z = 0; z < pmt->num_stat_boosts(); z++) {
|
||||
const auto& sb = pmt->get_stat_boost(z);
|
||||
static constexpr std::array<const char*, 0x10> stat_names{
|
||||
"ATP+", "ATA+", "EVP+", "DFP+", "MST+", "HP+", "LCK+", "ALL+",
|
||||
"ATP-", "ATA-", "EVP-", "DFP-", "MST-", "HP-", "LCK-", "ALL-"};
|
||||
string s;
|
||||
if (sb.stat1 > 0x10) {
|
||||
s = std::format("[{:02X}:{:04X}]", sb.stat1, sb.amount1);
|
||||
} else if (sb.stat1 > 0) {
|
||||
s = std::format("{}{}", stat_names[sb.stat1 - 1], sb.amount1);
|
||||
}
|
||||
if (sb.stat2) {
|
||||
if (!s.empty()) {
|
||||
s += ", ";
|
||||
}
|
||||
if (sb.stat2 > 0x10) {
|
||||
s += std::format("[{:02X}:{:04X}]", sb.stat2, sb.amount2);
|
||||
} else if (sb.stat2 > 0) {
|
||||
s += std::format("{}{}", stat_names[sb.stat2 - 1], sb.amount2);
|
||||
}
|
||||
}
|
||||
if (s.empty()) {
|
||||
s = "(none)";
|
||||
}
|
||||
phosg::fwrite_fmt(stream, " {:02X} => {}\n", z, s);
|
||||
}
|
||||
|
||||
phosg::fwrite_fmt(stream, "SHIELD EFFECTS\n");
|
||||
phosg::fwrite_fmt(stream, " ## => -SOUND1- ---A1---\n");
|
||||
for (size_t z = 0; z < pmt->num_shield_effects(); z++) {
|
||||
const auto& se = pmt->get_shield_effect(z);
|
||||
phosg::fwrite_fmt(stream, " {:02X} => {:08X} {:08X}\n", z, se.sound_id, se.unknown_a1);
|
||||
}
|
||||
|
||||
phosg::fwrite_fmt(stream, "SOUND REMAPS\n");
|
||||
phosg::fwrite_fmt(stream, " -SOUND1- => RT:[...] CC:[...]\n");
|
||||
for (const auto& remap : pmt->get_all_sound_remaps()) {
|
||||
std::string rt_str;
|
||||
for (uint32_t rt_sound_id : remap.by_rt_index) {
|
||||
if (!rt_str.empty()) {
|
||||
rt_str += ",";
|
||||
}
|
||||
rt_str += std::format("{:08X}", rt_sound_id);
|
||||
}
|
||||
std::string cc_str;
|
||||
for (uint32_t cc_sound_id : remap.by_char_class) {
|
||||
if (!cc_str.empty()) {
|
||||
cc_str += ",";
|
||||
}
|
||||
cc_str += std::format("{:08X}", cc_sound_id);
|
||||
}
|
||||
phosg::fwrite_fmt(stream, " {:08X} => RT:[{}] CC:[{}]\n", remap.sound_id, rt_str, cc_str);
|
||||
}
|
||||
|
||||
phosg::fwrite_fmt(stream, "TECH BOOSTS\n");
|
||||
phosg::fwrite_fmt(stream, " ## => BOOSTS\n");
|
||||
for (size_t z = 0; z < pmt->num_tech_boosts(); z++) {
|
||||
const auto& tb = pmt->get_tech_boost(z);
|
||||
string s;
|
||||
if (tb.amount1) {
|
||||
s += std::format("{:02X}:{:02X}:{:g}", tb.tech_num1, tb.flags1, tb.amount1);
|
||||
}
|
||||
if (tb.amount2) {
|
||||
s += std::format("{:02X}:{:02X}:{:g}", tb.tech_num2, tb.flags2, tb.amount2);
|
||||
}
|
||||
if (tb.amount3) {
|
||||
s += std::format("{:02X}:{:02X}:{:g}", tb.tech_num3, tb.flags3, tb.amount3);
|
||||
}
|
||||
phosg::fwrite_fmt(stream, " {:02X} => {}\n", z, s);
|
||||
}
|
||||
|
||||
phosg::fwrite_fmt(stream, "UNSEALABLE ITEMS\n");
|
||||
phosg::fwrite_fmt(stream, " -ITEM- NAME\n");
|
||||
std::vector<uint32_t> unsealable_items;
|
||||
for (uint32_t item_code : pmt->all_unsealable_items()) {
|
||||
unsealable_items.emplace_back(item_code);
|
||||
}
|
||||
std::sort(unsealable_items.begin(), unsealable_items.end());
|
||||
for (uint32_t item_code : unsealable_items) {
|
||||
ItemData item;
|
||||
item.data1[0] = item_code >> 16;
|
||||
item.data1[1] = item_code >> 8;
|
||||
item.data1[2] = item_code;
|
||||
string name = this->describe_item(item);
|
||||
phosg::fwrite_fmt(stream, " {:06X} {}\n", item_code, name);
|
||||
}
|
||||
|
||||
phosg::fwrite_fmt(stream, "RANGED SPECIALS\n");
|
||||
phosg::fwrite_fmt(stream, " ## => 11 12 WR A1\n");
|
||||
for (size_t z = 0; z < pmt->num_ranged_specials(); z++) {
|
||||
const auto& rs = pmt->get_ranged_special(z);
|
||||
phosg::fwrite_fmt(stream, " {:02X} => {:02X} {:02X} {:02X} {:02X}\n",
|
||||
z, rs.data1_1, rs.data1_2, rs.weapon_range_index, rs.unknown_a1);
|
||||
}
|
||||
}
|
||||
|
||||
+3191
-1079
File diff suppressed because it is too large
Load Diff
+347
-603
File diff suppressed because it is too large
Load Diff
@@ -46,18 +46,18 @@ ItemTranslationTable::ItemTranslationTable(
|
||||
uint32_t e_id = this->entries[z].id_for_version[v_s];
|
||||
if (is_canonical(e_id)) {
|
||||
if (!entry_index.count(e_id)) {
|
||||
throw logic_error(std::format("(row {} version {}) canonical ID {:X} is missing from the index", z, phosg::name_for_enum(v), e_id));
|
||||
throw logic_error(std::format("(row {} version {}) canonical ID {:08X} is missing from the index", z, phosg::name_for_enum(v), e_id));
|
||||
}
|
||||
try {
|
||||
item_parameter_table->definition_for_primary_identifier(e_id);
|
||||
} catch (const out_of_range&) {
|
||||
throw runtime_error(std::format("(row {} version {}) ID {:X} not defined in item parameter table", z, phosg::name_for_enum(v), e_id));
|
||||
throw runtime_error(std::format("(row {} version {}) ID {:08X} not defined in item parameter table", z, phosg::name_for_enum(v), e_id));
|
||||
}
|
||||
if (!remaining_identifiers.erase(e_id)) {
|
||||
throw runtime_error(std::format("(row {} version {}) ID {:X} not in item parameter table's primary identifier list", z, phosg::name_for_enum(v), e_id));
|
||||
throw runtime_error(std::format("(row {} version {}) ID {:08X} not in item parameter table's primary identifier list", z, phosg::name_for_enum(v), e_id));
|
||||
}
|
||||
} else if (!entry_index.count(make_canonical(e_id))) {
|
||||
throw runtime_error(std::format("(row {} version {}) ID {:X} refers to nonexistent canonical ID", z, phosg::name_for_enum(v), e_id));
|
||||
throw runtime_error(std::format("(row {} version {}) ID {:08X} refers to nonexistent canonical ID", z, phosg::name_for_enum(v), e_id));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -133,7 +133,8 @@ void player_use_item(shared_ptr<Client> c, size_t item_index, shared_ptr<RandomG
|
||||
|
||||
} else if ((primary_identifier & 0xFFFF0000) == 0x030C0000) { // Non-combo mag cells
|
||||
auto& mag = player->inventory.items[player->inventory.find_equipped_item(EquipSlot::MAG)];
|
||||
if (s->mag_evolution_table(c->version())->get_evolution_number(mag.data.data1[1]) < 4) {
|
||||
uint8_t evolution_number = s->mag_evolution_table(c->version())->get_evolution_number(mag.data.data1[1]);
|
||||
if (evolution_number < 4) {
|
||||
switch (item.data.data1[2]) {
|
||||
case 0x00: // Cell of MAG 502
|
||||
mag.data.data1[1] = (player->disp.visual.section_id & 1) ? 0x1D : 0x21;
|
||||
|
||||
+3
-9
@@ -33,11 +33,9 @@ struct CharacterStatsT {
|
||||
ret.lck = this->lck;
|
||||
return ret;
|
||||
}
|
||||
} __attribute__((packed));
|
||||
} __packed_ws_be__(CharacterStatsT, 0x0E);
|
||||
using CharacterStats = CharacterStatsT<false>;
|
||||
using CharacterStatsBE = CharacterStatsT<true>;
|
||||
check_struct_size(CharacterStats, 0x0E);
|
||||
check_struct_size(CharacterStatsBE, 0x0E);
|
||||
|
||||
template <bool BE>
|
||||
struct PlayerStatsT {
|
||||
@@ -61,11 +59,9 @@ struct PlayerStatsT {
|
||||
ret.meseta = this->meseta;
|
||||
return ret;
|
||||
}
|
||||
} __attribute__((packed));
|
||||
} __packed_ws_be__(PlayerStatsT, 0x24);
|
||||
using PlayerStats = PlayerStatsT<false>;
|
||||
using PlayerStatsBE = PlayerStatsT<true>;
|
||||
check_struct_size(PlayerStats, 0x24);
|
||||
check_struct_size(PlayerStatsBE, 0x24);
|
||||
|
||||
template <bool BE>
|
||||
struct LevelStatsDeltaT {
|
||||
@@ -89,11 +85,9 @@ struct LevelStatsDeltaT {
|
||||
ps.mst += this->mst;
|
||||
ps.lck += this->lck;
|
||||
}
|
||||
} __attribute__((packed));
|
||||
} __packed_ws_be__(LevelStatsDeltaT, 0x0C);
|
||||
using LevelStatsDelta = LevelStatsDeltaT<false>;
|
||||
using LevelStatsDeltaBE = LevelStatsDeltaT<true>;
|
||||
check_struct_size(LevelStatsDelta, 0x0C);
|
||||
check_struct_size(LevelStatsDeltaBE, 0x0C);
|
||||
|
||||
class LevelTable {
|
||||
// This is the base class for all the LevelTable implementations. The public interface here only defines functions
|
||||
|
||||
@@ -145,6 +145,7 @@ uint32_t Lobby::FloorItemManager::reassign_all_item_ids(uint32_t next_item_id) {
|
||||
Lobby::Lobby(shared_ptr<ServerState> s, uint32_t id, bool is_game)
|
||||
: server_state(s),
|
||||
log(std::format("[{}:{:X}] ", is_game ? "Game" : "Lobby", id), lobby_log.min_level),
|
||||
creation_time(phosg::now()),
|
||||
lobby_id(id),
|
||||
random_seed(phosg::random_object<uint32_t>()),
|
||||
rand_crypt(make_shared<DisabledRandomGenerator>()),
|
||||
@@ -229,6 +230,14 @@ void Lobby::create_item_creator(Version logic_version) {
|
||||
effective_section_id,
|
||||
rand_crypt,
|
||||
this->quest ? this->quest->meta.battle_rules : nullptr);
|
||||
if (this->blueballz_tier >= 0) {
|
||||
double rare_mult = 1.25 + (static_cast<double>(this->blueballz_tier) * 0.25);
|
||||
this->item_creator->set_rare_drop_rate_multiplier(rare_mult);
|
||||
this->log.info_f("Blueballz +{} rare drop rate multiplier set to {:g}x", this->blueballz_tier, rare_mult);
|
||||
}
|
||||
if (s->use_legacy_item_random_behavior) {
|
||||
this->item_creator->set_legacy_replay();
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t Lobby::effective_section_id() const {
|
||||
@@ -831,3 +840,35 @@ bool Lobby::compare_shared(const shared_ptr<const Lobby>& a, const shared_ptr<co
|
||||
|
||||
return a->name < b->name;
|
||||
}
|
||||
|
||||
template <>
|
||||
const char* phosg::name_for_enum<Lobby::JoinError>(Lobby::JoinError value) {
|
||||
switch (value) {
|
||||
case Lobby::JoinError::ALLOWED:
|
||||
return "ALLOWED";
|
||||
case Lobby::JoinError::FULL:
|
||||
return "FULL";
|
||||
case Lobby::JoinError::VERSION_CONFLICT:
|
||||
return "VERSION_CONFLICT";
|
||||
case Lobby::JoinError::QUEST_SELECTION_IN_PROGRESS:
|
||||
return "QUEST_SELECTION_IN_PROGRESS";
|
||||
case Lobby::JoinError::QUEST_IN_PROGRESS:
|
||||
return "QUEST_IN_PROGRESS";
|
||||
case Lobby::JoinError::BATTLE_IN_PROGRESS:
|
||||
return "BATTLE_IN_PROGRESS";
|
||||
case Lobby::JoinError::LOADING:
|
||||
return "LOADING";
|
||||
case Lobby::JoinError::SOLO:
|
||||
return "SOLO";
|
||||
case Lobby::JoinError::INCORRECT_PASSWORD:
|
||||
return "INCORRECT_PASSWORD";
|
||||
case Lobby::JoinError::LEVEL_TOO_LOW:
|
||||
return "LEVEL_TOO_LOW";
|
||||
case Lobby::JoinError::LEVEL_TOO_HIGH:
|
||||
return "LEVEL_TOO_HIGH";
|
||||
case Lobby::JoinError::NO_ACCESS_TO_QUEST:
|
||||
return "NO_ACCESS_TO_QUEST";
|
||||
default:
|
||||
throw runtime_error("invalid drop mode");
|
||||
}
|
||||
}
|
||||
|
||||
+5
-3
@@ -81,6 +81,7 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
|
||||
START_BATTLE_PLAYER_IMMEDIATELY = 0x00010000,
|
||||
CANNOT_CHANGE_CHEAT_MODE = 0x00020000,
|
||||
USE_CREATOR_SECTION_ID = 0x00040000,
|
||||
BLUEBALLZ_PLUS0 = 0x00080000,
|
||||
// Flags used only for lobbies
|
||||
PUBLIC = 0x01000000,
|
||||
DEFAULT = 0x02000000,
|
||||
@@ -91,6 +92,8 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
|
||||
std::weak_ptr<ServerState> server_state;
|
||||
phosg::PrefixedLogger log;
|
||||
|
||||
uint64_t creation_time;
|
||||
|
||||
uint32_t lobby_id;
|
||||
|
||||
uint32_t min_level = 0;
|
||||
@@ -116,6 +119,7 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
|
||||
Episode episode = Episode::NONE;
|
||||
GameMode mode = GameMode::NORMAL;
|
||||
Difficulty difficulty = Difficulty::NORMAL;
|
||||
int8_t blueballz_tier = -1; // -1 = disabled; 0..10 = Blueballz +0..+10
|
||||
float base_exp_multiplier = 1.0f;
|
||||
float exp_share_multiplier = 0.5f;
|
||||
float challenge_exp_multiplier = 1.0f;
|
||||
@@ -277,6 +281,4 @@ struct Lobby : public std::enable_shared_from_this<Lobby> {
|
||||
};
|
||||
|
||||
template <>
|
||||
ServerDropMode phosg::enum_for_name<ServerDropMode>(const char* name);
|
||||
template <>
|
||||
const char* phosg::name_for_enum<ServerDropMode>(ServerDropMode value);
|
||||
const char* phosg::name_for_enum<Lobby::JoinError>(Lobby::JoinError value);
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
#include "MagEvolutionTable.hh"
|
||||
|
||||
#include "CommonFileFormats.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
struct MotionReference {
|
||||
struct Side {
|
||||
// This specifies which entry in ItemMagMotion.dat is used. The file is just a list of 0x64-byte structures.
|
||||
// 0xFF = no TItemMagSub is created
|
||||
uint8_t motion_table_entry = 0xFF;
|
||||
parray<uint8_t, 5> unknown_a1 = 0;
|
||||
} __packed_ws__(Side, 0x06);
|
||||
parray<Side, 2> sides; // [0] = right side, [1] = left side
|
||||
} __packed_ws__(MotionReference, 0x0C);
|
||||
|
||||
template <bool BE>
|
||||
struct MotionReferenceTables {
|
||||
// It seems that there are two definition tables, but only the first is used on any version of PSO. On v3 and later,
|
||||
// the two offsets point to the same table, but on v2 they don't and the second table contains different data.
|
||||
// TODO: Figure out what the deal is with the different v2 tables.
|
||||
U32T<BE> ref_table; // -> MotionReference[num_mags]
|
||||
U32T<BE> unused_ref_table; // -> MotionReference[num_mags]
|
||||
} __packed_ws_be__(MotionReferenceTables, 0x08);
|
||||
|
||||
template <bool BE>
|
||||
struct ColorEntry {
|
||||
// Colors are specified as 4 floats, each in the range [0, 1], for each color channel. The default colors are:
|
||||
// alpha red green blue color (see StaticGameData.cc)
|
||||
// 1.0 1.0 0.2 0.1 red
|
||||
// 1.0 0.2 0.2 1.0 blue
|
||||
// 1.0 1.0 0.9 0.1 yellow
|
||||
// 1.0 0.1 1.0 0.1 green
|
||||
// 1.0 0.8 0.1 1.0 purple
|
||||
// 1.0 0.1 0.1 0.2 black
|
||||
// 1.0 0.9 1.0 1.0 white
|
||||
// 1.0 0.1 0.9 1.0 cyan
|
||||
// 1.0 0.5 0.3 0.2 brown
|
||||
// 1.0 1.0 0.4 0.0 orange (v3+)
|
||||
// 1.0 0.502 0.545 0.977 light-blue (v3+)
|
||||
// 1.0 0.502 0.502 0.0 olive (v3+)
|
||||
// 1.0 0.0 0.941 0.714 turquoise (v3+)
|
||||
// 1.0 0.8 0.098 0.392 fuchsia (v3+)
|
||||
// 1.0 0.498 0.498 0.498 grey (v3+)
|
||||
// 1.0 0.996 0.996 0.832 cream (v3+)
|
||||
// 1.0 0.996 0.498 0.784 pink (v3+)
|
||||
// 1.0 0.0 0.498 0.322 dark-green (v3+)
|
||||
F32T<BE> alpha;
|
||||
F32T<BE> red;
|
||||
F32T<BE> green;
|
||||
F32T<BE> blue;
|
||||
} __packed_ws_be__(ColorEntry, 0x10);
|
||||
|
||||
template <bool BE>
|
||||
struct UnknownA3Entry {
|
||||
uint8_t flags;
|
||||
uint8_t unknown_a2;
|
||||
U16T<BE> unknown_a3;
|
||||
U16T<BE> unknown_a4;
|
||||
U16T<BE> unknown_a5;
|
||||
} __packed_ws_be__(UnknownA3Entry, 0x08);
|
||||
|
||||
template <bool BE>
|
||||
struct RootV2V3V4 {
|
||||
/* -- / 112K / V1 / V2 / V3 / BB */
|
||||
/* 00 / 0438 / 0438 / 05BC / 0340 / 0400 */ U32T<BE> motion_tables; // -> MotionReferenceTables
|
||||
/* 04 / 0440 / 0440 / 0594 / 0348 / 0408 */ U32T<BE> unknown_a2; // -> (uint8_t[2])[NumMags] (references into unknown_a3)
|
||||
/* 08 / 0498 / 0498 / 0608 / 03CE / 04AE */ U32T<BE> unknown_a3; // -> UnknownA3Entry[max(unknown_a2) + 1]
|
||||
/* 0C / 0510 / 0520 / 06B0 / 0476 / 0556 */ U32T<BE> unknown_a4; // -> uint8_t[NumMags]
|
||||
/* 10 / 053C / 054C / 06EC / 04BC / 05AC */ U32T<BE> color_table; // -> ColorEntry[NumColors]
|
||||
/* 14 / / / 077C / 05DC / 06CC */ U32T<BE> evolution_number_table; // -> uint8_t[NumMags]
|
||||
} __packed_ws_be__(RootV2V3V4, 0x18);
|
||||
|
||||
struct RootV1 {
|
||||
le_uint32_t motion_tables;
|
||||
le_uint32_t unknown_a2;
|
||||
le_uint32_t unknown_a3;
|
||||
le_uint32_t unknown_a4;
|
||||
le_uint32_t color_table;
|
||||
} __packed_ws__(RootV1, 0x14);
|
||||
|
||||
static uint8_t get_v1_mag_evolution_number(uint8_t data1_1) {
|
||||
static const std::array<uint8_t, 0x2C> v1_evolution_number_table{
|
||||
/* 00 */ 0, 1, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 2, 2,
|
||||
/* 10 */ 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 2, 2, 3, 4, 3, 3,
|
||||
/* 20 */ 3, 4, 4, 3, 3, 3, 3, 4, 4, 4, 4, 4};
|
||||
if (data1_1 >= v1_evolution_number_table.size()) {
|
||||
throw runtime_error("invalid mag number");
|
||||
}
|
||||
return v1_evolution_number_table[data1_1];
|
||||
}
|
||||
|
||||
template <typename RootT, size_t NumMags, size_t NumColors, bool BE>
|
||||
class MagEvolutionTableT : public MagEvolutionTable {
|
||||
public:
|
||||
explicit MagEvolutionTableT(std::shared_ptr<const std::string> data)
|
||||
: data(data), r(*data), root(&r.pget<RootT>(this->r.pget_u32l(this->data->size() - 0x10))) {}
|
||||
virtual ~MagEvolutionTableT() = default;
|
||||
|
||||
virtual VectorXYZTF get_color_rgba(size_t index) const {
|
||||
if (index >= NumColors) {
|
||||
throw runtime_error("invalid mag color index");
|
||||
}
|
||||
const auto& color = this->r.pget<ColorEntry<BE>>(this->root->color_table + sizeof(ColorEntry<BE>) * index);
|
||||
return {color.red.load(), color.green.load(), color.blue.load(), color.alpha.load()};
|
||||
}
|
||||
|
||||
virtual uint8_t get_evolution_number(uint8_t data1_1) const {
|
||||
if constexpr (requires { this->root->evolution_number_table; }) {
|
||||
return this->r.pget_u8(this->root->evolution_number_table + data1_1);
|
||||
} else {
|
||||
return get_v1_mag_evolution_number(data1_1);
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
std::shared_ptr<const std::string> data;
|
||||
phosg::StringReader r;
|
||||
const RootT* root;
|
||||
};
|
||||
|
||||
class MagEvolutionTableDCNTE : public MagEvolutionTable {
|
||||
public:
|
||||
MagEvolutionTableDCNTE() = default;
|
||||
virtual ~MagEvolutionTableDCNTE() = default;
|
||||
|
||||
virtual VectorXYZTF get_color_rgba(size_t) const {
|
||||
throw runtime_error("mag colors not available on DC NTE");
|
||||
}
|
||||
|
||||
virtual uint8_t get_evolution_number(uint8_t data1_1) const {
|
||||
return get_v1_mag_evolution_number(data1_1);
|
||||
}
|
||||
};
|
||||
|
||||
using MagEvolutionTableDC112000 = MagEvolutionTableT<RootV2V3V4<false>, 0x28, 0x09, false>;
|
||||
using MagEvolutionTableV1 = MagEvolutionTableT<RootV2V3V4<false>, 0x28, 0x09, false>;
|
||||
using MagEvolutionTableV2 = MagEvolutionTableT<RootV2V3V4<false>, 0x3A, 0x09, false>;
|
||||
using MagEvolutionTableGCNTE = MagEvolutionTableT<RootV2V3V4<true>, 0x3A, 0x09, true>;
|
||||
using MagEvolutionTableGC = MagEvolutionTableT<RootV2V3V4<true>, 0x43, 0x12, true>;
|
||||
using MagEvolutionTableXB = MagEvolutionTableT<RootV2V3V4<false>, 0x43, 0x12, false>;
|
||||
using MagEvolutionTableV4 = MagEvolutionTableT<RootV2V3V4<false>, 0x53, 0x12, false>;
|
||||
|
||||
std::shared_ptr<MagEvolutionTable> MagEvolutionTable::create(
|
||||
std::shared_ptr<const std::string> data, Version version) {
|
||||
switch (version) {
|
||||
case Version::DC_NTE:
|
||||
return std::make_shared<MagEvolutionTableDCNTE>();
|
||||
case Version::DC_11_2000:
|
||||
return std::make_shared<MagEvolutionTableDC112000>(data);
|
||||
case Version::DC_V1:
|
||||
return std::make_shared<MagEvolutionTableV1>(data);
|
||||
case Version::DC_V2:
|
||||
case Version::PC_NTE:
|
||||
case Version::PC_V2:
|
||||
return std::make_shared<MagEvolutionTableV2>(data);
|
||||
case Version::GC_NTE:
|
||||
return std::make_shared<MagEvolutionTableGCNTE>(data);
|
||||
case Version::GC_V3:
|
||||
case Version::GC_EP3:
|
||||
case Version::GC_EP3_NTE:
|
||||
return std::make_shared<MagEvolutionTableGC>(data);
|
||||
case Version::XB_V3:
|
||||
return std::make_shared<MagEvolutionTableXB>(data);
|
||||
case Version::BB_V4:
|
||||
return std::make_shared<MagEvolutionTableV4>(data);
|
||||
default:
|
||||
throw std::logic_error("Cannot create mag evolution table for this version");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include "WindowsPlatform.hh"
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "CommonFileFormats.hh"
|
||||
#include "Text.hh"
|
||||
#include "Types.hh"
|
||||
#include "Version.hh"
|
||||
|
||||
class MagEvolutionTable {
|
||||
public:
|
||||
virtual ~MagEvolutionTable() = default;
|
||||
|
||||
static std::shared_ptr<MagEvolutionTable> create(std::shared_ptr<const std::string> data, Version version);
|
||||
|
||||
virtual VectorXYZTF get_color_rgba(size_t index) const = 0;
|
||||
virtual uint8_t get_evolution_number(uint8_t data1_1) const = 0;
|
||||
|
||||
protected:
|
||||
MagEvolutionTable() = default;
|
||||
};
|
||||
+140
-69
@@ -464,7 +464,7 @@ Action a_psov2_encrypt_single_test(
|
||||
current_value, num_mismatches, phosg::format_duration(crypt_time), phosg::format_duration(single_time),
|
||||
static_cast<float>(crypt_time) / single_time);
|
||||
};
|
||||
phosg::parallel_range_blocks<uint64_t>(thread_fn, 0, 0x100000000, 0x1000, num_threads, progress_fn);
|
||||
phosg::parallel_blocks<uint64_t>(thread_fn, 0, 0x100000000, 0x1000, num_threads, progress_fn);
|
||||
|
||||
progress_fn(0, 0, 0xFFFFFFFF, 0);
|
||||
});
|
||||
@@ -813,7 +813,7 @@ static void a_encrypt_decrypt_vms_save_fn(phosg::Arguments& args) {
|
||||
}
|
||||
|
||||
} else {
|
||||
uint64_t seed = phosg::parallel_range_blocks<uint64_t>([&](uint64_t serial_number, size_t) -> bool {
|
||||
uint64_t seed = phosg::parallel_blocks<uint64_t>([&](uint64_t serial_number, size_t) -> bool {
|
||||
try {
|
||||
auto decrypted = decrypt_fixed_size_data_section_t<StructT, false, ChecksumLength>(
|
||||
data_section, sizeof(StructT), serial_number, skip_checksum, override_round2_seed);
|
||||
@@ -1136,9 +1136,8 @@ Action a_decrypt_dcv2_executable(
|
||||
executable (DP_ADDRESS.JPN), INDEXES should be the path to the index fixup\n\
|
||||
table (KATSUO.SEA), and VALUES should be the path to the value fixup table\n\
|
||||
(IWASHI.SEA). The output is written to EXEC.dec.\n\
|
||||
If --simple is given, uses the simpler encryption method used in some\n\
|
||||
community modifications of the game (Enhancement Pack, for example). In\n\
|
||||
this case, --seed is not required; if not given, finds the seed\n\
|
||||
If --simple is given, uses the encryption method used in Ives\' Enhancement\n\
|
||||
Pack. In this case, --seed is not required; if not given, finds the seed\n\
|
||||
automatically, and prints it to stderr so you will be able to use it when\n\
|
||||
re-encrypting.\n",
|
||||
+[](phosg::Arguments& args) {
|
||||
@@ -1166,8 +1165,8 @@ Action a_encrypt_dcv2_executable(
|
||||
executable (DP_ADDRESS.JPN) and INDEXES should be the path to the index\n\
|
||||
fixup table (KATSUO.SEA). The output is written to EXEC.enc and\n\
|
||||
INDEXES.enc.\n\
|
||||
If --simple is given, uses the simpler encryption method used in some\n\
|
||||
community modifications of the game. In this case, --seed is required.\n",
|
||||
If --simple is given, uses the simpler encryption method used in Ives\'\n\
|
||||
Enhancement Pack. In this case, --seed is required.\n",
|
||||
+[](phosg::Arguments& args) {
|
||||
string executable_filename = args.get<string>("executable", true);
|
||||
string executable_data = phosg::load_file(executable_filename);
|
||||
@@ -1188,7 +1187,7 @@ Action a_encrypt_dcv2_executable(
|
||||
Action a_decode_gci_snapshot(
|
||||
"decode-gci-snapshot", "\
|
||||
decode-gci-snapshot [INPUT-FILENAME [OUTPUT-FILENAME]]\n\
|
||||
Decode a PSO GC snapshot file into a Windows BMP image.\n",
|
||||
Decode a PSO GC snapshot file (in GCI format) into a Windows BMP image.\n",
|
||||
+[](phosg::Arguments& args) {
|
||||
auto data = read_input_data(args);
|
||||
phosg::StringReader r(data);
|
||||
@@ -1366,7 +1365,7 @@ Action a_salvage_gci(
|
||||
};
|
||||
|
||||
if (!round2) {
|
||||
phosg::parallel_range_blocks<uint64_t>(
|
||||
phosg::parallel_blocks<uint64_t>(
|
||||
[&](uint64_t seed, size_t thread_num) -> bool {
|
||||
auto decrypted = decrypt_fixed_size_data_section_t<StructT, true>(
|
||||
data_section, header.data_size, seed, true);
|
||||
@@ -1386,7 +1385,7 @@ Action a_salvage_gci(
|
||||
// high half, which is much faster than trying all possible seeds. Unfortunately, this relies on the
|
||||
// distribution of values in the plaintext, so it only works for the round-2 seed - the decrypted data after
|
||||
// round 1 is still essentially random.
|
||||
phosg::parallel_range_blocks<uint64_t>(try_round2_seed, 0, 0x100000, 0x1000, num_threads);
|
||||
phosg::parallel_blocks<uint64_t>(try_round2_seed, 0, 0x100000, 0x1000, num_threads);
|
||||
auto intermediate_top_seeds = merge_top_seeds(top_seeds_by_thread);
|
||||
if (intermediate_top_seeds.empty()) {
|
||||
throw logic_error("no intermediate seeds were found");
|
||||
@@ -1397,7 +1396,7 @@ Action a_salvage_gci(
|
||||
for (auto& top_seeds : top_seeds_by_thread) {
|
||||
top_seeds.clear();
|
||||
}
|
||||
phosg::parallel_range_blocks<uint64_t>(
|
||||
phosg::parallel_blocks<uint64_t>(
|
||||
[&](uint64_t seed, size_t thread_num) -> bool {
|
||||
return try_round2_seed((seed << 16) | round2_lower_half, thread_num);
|
||||
},
|
||||
@@ -1405,7 +1404,7 @@ Action a_salvage_gci(
|
||||
|
||||
} else {
|
||||
// The user requested not to take any shortcuts, so burn a lot of CPU power
|
||||
phosg::parallel_range_blocks<uint64_t>(try_round2_seed, 0, 0x100000000, 0x1000, num_threads);
|
||||
phosg::parallel_blocks<uint64_t>(try_round2_seed, 0, 0x100000000, 0x1000, num_threads);
|
||||
}
|
||||
|
||||
print_top_seeds(merge_top_seeds(top_seeds_by_thread));
|
||||
@@ -1474,7 +1473,7 @@ Action a_find_decryption_seed(
|
||||
return true;
|
||||
};
|
||||
|
||||
uint64_t seed = phosg::parallel_range_blocks<uint64_t>([&](uint64_t seed, size_t) -> bool {
|
||||
uint64_t seed = phosg::parallel_blocks<uint64_t>([&](uint64_t seed, size_t) -> bool {
|
||||
string be_decrypt_buf = ciphertext.substr(0, max_plaintext_size);
|
||||
string le_decrypt_buf = ciphertext.substr(0, max_plaintext_size);
|
||||
if (uses_v3_encryption(version)) {
|
||||
@@ -1622,13 +1621,17 @@ Action a_disassemble_quest_script(
|
||||
disassemble-quest-script [OPTIONS] [INPUT-FILENAME [OUTPUT-FILENAME]]\n\
|
||||
Disassemble the input quest script (.bin file) into a text representation\n\
|
||||
of the commands and metadata it contains. Specify the quest\'s game version\n\
|
||||
with one of the --dc-nte, --dc-v1, --dc-v2, --pc, --gc-nte, --gc, --gc-ep3,\n\
|
||||
--xb, or --bb options. newserv uses more descriptive opcode mnemonics by\n\
|
||||
default; the --qedit option will result in names matching those used by\n\
|
||||
QEdit. If you intend to reassemble the script, after editing it, use the\n\
|
||||
--reassembly option to add explicit label numbers and remove offsets and\n\
|
||||
data in code sections. To include script references from the map, use the\n\
|
||||
--map-file=FILENAME option.\n",
|
||||
with one of the --dc-nte, --dc-v1, --dc-v2, --pc, --gc-nte, --gc,\n\
|
||||
--gc-ep3, --xb, or --bb options. Other options:\n\
|
||||
--qedit: newserv uses more descriptive opcode mnemonics by default; this\n\
|
||||
option will result in names matching those used by QEdit.\n\
|
||||
--reassembly: If you intend to reassemble the script after editing it,\n\
|
||||
use this option to add explicit label numbers and remove offsets and\n\
|
||||
data in code sections.\n\
|
||||
--map-file=FILENAME: Include references to script labels from this map.\n\
|
||||
--language=L: Decode strings using this language. L may be J, E, G, F,\n\
|
||||
S, B, T, or K, for Japanese, English, German, French, Spanish,\n\
|
||||
Simplified Chinese, Traditional Chinese, or Korean respectively.\n",
|
||||
+[](phosg::Arguments& args) {
|
||||
string data = read_input_data(args);
|
||||
auto version = get_cli_version(args);
|
||||
@@ -1641,7 +1644,8 @@ Action a_disassemble_quest_script(
|
||||
auto map_data = make_shared<string>(prs_decompress(phosg::load_file(map_filename)));
|
||||
map_file = make_shared<MapFile>(map_data);
|
||||
}
|
||||
Language language = static_cast<Language>(args.get<uint8_t>("language", 0xFF));
|
||||
const auto& language_str = args.get<string>("language");
|
||||
Language language = language_str.empty() ? Language::ENGLISH : language_for_name(language_str);
|
||||
bool reassembly_mode = args.get<bool>("reassembly");
|
||||
bool use_qedit_names = args.get<bool>("qedit");
|
||||
string result = disassemble_quest_script(
|
||||
@@ -1917,8 +1921,7 @@ Action a_extract_ppk("extract-ppk", "\
|
||||
PC/BB format. For PPK archives, the --password= option is required.\n",
|
||||
a_extract_archive_fn);
|
||||
|
||||
Action a_encode_sjis(
|
||||
"transcode-text", nullptr, +[](phosg::Arguments& args) {
|
||||
Action a_transcode_text("transcode-text", nullptr, +[](phosg::Arguments& args) {
|
||||
TextTranscoder* tt_from = nullptr;
|
||||
{
|
||||
std::string from_name = args.get<std::string>("from");
|
||||
@@ -1958,8 +1961,7 @@ Action a_encode_sjis(
|
||||
if (tt_to) {
|
||||
data = (*tt_to)(data);
|
||||
}
|
||||
write_output_data(args, data.data(), data.size(), "txt");
|
||||
});
|
||||
write_output_data(args, data.data(), data.size(), "txt"); });
|
||||
|
||||
Action a_decode_text_archive(
|
||||
"decode-text-archive", "\
|
||||
@@ -2199,6 +2201,25 @@ Action a_download_files(
|
||||
io_context->run();
|
||||
});
|
||||
|
||||
std::shared_ptr<RareItemSet> load_rare_item_set(
|
||||
const std::string& filename, bool is_v1, std::shared_ptr<const ItemNameIndex> v4_item_name_index) {
|
||||
string filename_lower = phosg::tolower(filename);
|
||||
auto data = make_shared<string>(phosg::load_file(filename));
|
||||
if (filename_lower.ends_with(".json")) {
|
||||
return make_shared<RareItemSet>(phosg::JSON::parse(*data), v4_item_name_index);
|
||||
} else if (filename_lower.ends_with(".gsl")) {
|
||||
return make_shared<RareItemSet>(GSLArchive(data, false), false);
|
||||
} else if (filename_lower.ends_with(".gslb")) {
|
||||
return make_shared<RareItemSet>(GSLArchive(data, true), true);
|
||||
} else if (filename_lower.ends_with(".afs")) {
|
||||
return make_shared<RareItemSet>(AFSArchive(data), is_v1);
|
||||
} else if (filename_lower.ends_with(".rel")) {
|
||||
return make_shared<RareItemSet>(*data, true);
|
||||
} else {
|
||||
throw runtime_error("cannot determine input format; use a filename ending with .json, .gsl, .gslb, .afs, or .rel");
|
||||
}
|
||||
}
|
||||
|
||||
Action a_convert_rare_item_set(
|
||||
"convert-rare-item-set", "\
|
||||
convert-rare-item-set INPUT-FILENAME [OUTPUT-FILENAME] [OPTIONS]\n\
|
||||
@@ -2228,24 +2249,8 @@ Action a_convert_rare_item_set(
|
||||
if (input_filename.empty() || (input_filename == "-")) {
|
||||
throw runtime_error("input filename must be given");
|
||||
}
|
||||
|
||||
string input_filename_lower = phosg::tolower(input_filename);
|
||||
auto data = make_shared<string>(read_input_data(args));
|
||||
shared_ptr<RareItemSet> rs;
|
||||
if (input_filename_lower.ends_with(".json")) {
|
||||
rs = make_shared<RareItemSet>(phosg::JSON::parse(*data), s->item_name_index_opt(get_cli_version(args, Version::BB_V4)));
|
||||
} else if (input_filename_lower.ends_with(".gsl")) {
|
||||
rs = make_shared<RareItemSet>(GSLArchive(data, false), false);
|
||||
} else if (input_filename_lower.ends_with(".gslb")) {
|
||||
rs = make_shared<RareItemSet>(GSLArchive(data, true), true);
|
||||
} else if (input_filename_lower.ends_with(".afs")) {
|
||||
rs = make_shared<RareItemSet>(AFSArchive(data), is_v1(get_cli_version(args, Version::DC_V2)));
|
||||
} else if (input_filename_lower.ends_with(".rel")) {
|
||||
rs = make_shared<RareItemSet>(*data, true);
|
||||
} else {
|
||||
throw runtime_error("cannot determine input format; use a filename ending with .json, .gsl, .gslb, .afs, or .rel");
|
||||
}
|
||||
|
||||
auto rs = load_rare_item_set(
|
||||
input_filename, is_v1(get_cli_version(args, Version::BB_V4)), s->item_name_index(Version::BB_V4));
|
||||
if (rate_factor != 1.0) {
|
||||
rs->multiply_all_rates(rate_factor);
|
||||
}
|
||||
@@ -2289,6 +2294,32 @@ Action a_convert_rare_item_set(
|
||||
throw runtime_error("cannot determine output format; use a filename ending with .json, .gsl, .gslb, or .afs");
|
||||
}
|
||||
});
|
||||
Action a_compare_rare_item_set(
|
||||
"compare-rare-item-set", nullptr,
|
||||
+[](phosg::Arguments& args) {
|
||||
string input_filename1 = args.get<string>(1, false);
|
||||
if (input_filename1.empty() || (input_filename1 == "-")) {
|
||||
throw runtime_error("two input filenames must be given");
|
||||
}
|
||||
string input_filename2 = args.get<string>(2, false);
|
||||
if (input_filename2.empty() || (input_filename2 == "-")) {
|
||||
throw runtime_error("two input filenames must be given");
|
||||
}
|
||||
|
||||
auto s = make_shared<ServerState>(get_config_filename(args));
|
||||
s->load_config_early();
|
||||
s->load_patch_indexes();
|
||||
s->load_text_index();
|
||||
s->load_item_definitions();
|
||||
s->load_item_name_indexes();
|
||||
s->load_drop_tables();
|
||||
|
||||
bool is_v1 = ::is_v1(get_cli_version(args, Version::BB_V4));
|
||||
auto rs1 = load_rare_item_set(input_filename1, is_v1, s->item_name_index(Version::BB_V4));
|
||||
auto rs2 = load_rare_item_set(input_filename2, is_v1, s->item_name_index(Version::BB_V4));
|
||||
|
||||
rs1->print_diff(stdout, *rs2);
|
||||
});
|
||||
|
||||
static shared_ptr<CommonItemSet> load_common_item_set(
|
||||
const std::string& filename, const std::string& ct_filename, bool big_endian) {
|
||||
@@ -2356,6 +2387,47 @@ Action a_compare_common_item_set(
|
||||
cs1->print_diff(stdout, *cs2);
|
||||
});
|
||||
|
||||
Action a_decode_item_parameter_table(
|
||||
"decode-item-parameter-table", "\
|
||||
decode-item-parameter-table [INPUT-FILENAME [OUTPUT-FILENAME]] [OPTIONS...]\n\
|
||||
Converts an ItemPMT file into a JSON item parameter table. A version\n\
|
||||
option is required. Use --hex to make item codes in the output readable;\n\
|
||||
however, this option also uses nonstandard JSON syntax - newserv can parse\n\
|
||||
it, but many other JSON parsers can\'t. Expects compressed input (a .prs\n\
|
||||
file) by default; use --decompressed if the input is not compressed.\n",
|
||||
+[](phosg::Arguments& args) {
|
||||
auto input_data = read_input_data(args);
|
||||
if (!args.get<bool>("decompressed")) {
|
||||
input_data = prs_decompress(input_data);
|
||||
}
|
||||
auto data = std::make_shared<string>(std::move(input_data));
|
||||
auto pmt = ItemParameterTable::from_binary(data, get_cli_version(args, Version::BB_V4));
|
||||
auto json = pmt->json();
|
||||
uint32_t serialize_options = phosg::JSON::SerializeOption::FORMAT | phosg::JSON::SerializeOption::SORT_DICT_KEYS;
|
||||
if (args.get<bool>("hex")) {
|
||||
serialize_options |= phosg::JSON::SerializeOption::HEX_INTEGERS;
|
||||
}
|
||||
string json_data = json.serialize(serialize_options);
|
||||
write_output_data(args, json_data.data(), json_data.size(), nullptr);
|
||||
});
|
||||
|
||||
Action a_encode_item_parameter_table(
|
||||
"encode-item-parameter-table", "\
|
||||
encode-item-parameter-table [INPUT-FILENAME [OUTPUT-FILENAME]] [OPTIONS...]\n\
|
||||
Converts a JSON item parameter table into an ItemPMT file compatible with\n\
|
||||
the game client. A version option is required. By default the output will\n\
|
||||
be compressed, as the client expects; use --decompressed to get\n\
|
||||
uncompressed output.\n",
|
||||
+[](phosg::Arguments& args) {
|
||||
auto json = phosg::JSON::parse(read_input_data(args));
|
||||
auto pmt = ItemParameterTable::from_json(json);
|
||||
string data = pmt->serialize_binary(get_cli_version(args, Version::BB_V4));
|
||||
if (!args.get<bool>("decompressed")) {
|
||||
data = prs_compress_optimal(data);
|
||||
}
|
||||
write_output_data(args, data.data(), data.size(), nullptr);
|
||||
});
|
||||
|
||||
Action a_describe_item(
|
||||
"describe-item", "\
|
||||
describe-item DATA-OR-DESCRIPTION\n\
|
||||
@@ -2759,26 +2831,26 @@ Action a_generate_ep3_cards_html(
|
||||
}
|
||||
}
|
||||
|
||||
phosg::parallel_range<uint32_t>([&](uint32_t index, size_t) -> bool {
|
||||
auto& info = this->card_infos[index];
|
||||
if (!info.large_filename.empty()) {
|
||||
auto img = phosg::ImageRGBA8888N::from_file_data(phosg::load_file(info.large_filename));
|
||||
img.resize(512, 399);
|
||||
info.large_data_url = img.serialize(phosg::ImageFormat::PNG_DATA_URL);
|
||||
}
|
||||
if (!info.medium_filename.empty()) {
|
||||
auto img = phosg::ImageRGBA8888N::from_file_data(phosg::load_file(info.medium_filename));
|
||||
img.resize(184, 144);
|
||||
info.medium_data_url = img.serialize(phosg::ImageFormat::PNG_DATA_URL);
|
||||
}
|
||||
if (!info.small_filename.empty()) {
|
||||
auto img = phosg::ImageRGBA8888N::from_file_data(phosg::load_file(info.small_filename));
|
||||
img.resize(58, 43);
|
||||
info.small_data_url = img.serialize(phosg::ImageFormat::PNG_DATA_URL);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
0, this->card_infos.size(), num_threads);
|
||||
phosg::parallel_range(
|
||||
this->card_infos, [&](CardInfo& info, size_t) -> bool {
|
||||
if (!info.large_filename.empty()) {
|
||||
auto img = phosg::ImageRGBA8888N::from_file_data(phosg::load_file(info.large_filename));
|
||||
img.resize(512, 399);
|
||||
info.large_data_url = img.serialize(phosg::ImageFormat::PNG_DATA_URL);
|
||||
}
|
||||
if (!info.medium_filename.empty()) {
|
||||
auto img = phosg::ImageRGBA8888N::from_file_data(phosg::load_file(info.medium_filename));
|
||||
img.resize(184, 144);
|
||||
info.medium_data_url = img.serialize(phosg::ImageFormat::PNG_DATA_URL);
|
||||
}
|
||||
if (!info.small_filename.empty()) {
|
||||
auto img = phosg::ImageRGBA8888N::from_file_data(phosg::load_file(info.small_filename));
|
||||
img.resize(58, 43);
|
||||
info.small_data_url = img.serialize(phosg::ImageFormat::PNG_DATA_URL);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
num_threads);
|
||||
}
|
||||
|
||||
this->num_output_columns = 1 + (!no_disassembly) + this->show_small_column + this->show_medium_column + this->show_large_column;
|
||||
@@ -3093,8 +3165,7 @@ Action a_check_supermaps(
|
||||
auto f = phosg::fopen_unique(filename, "wt");
|
||||
phosg::fwrite_fmt(f.get(), "QUEST {} ({})\n", it.first, it.second->meta.name);
|
||||
phosg::fwrite_fmt(f.get(), "ENEMY--------------- DCNTE 11/2K DC-V1 DC-V2 PCNTE PC-V2 GCNTE GC-V3 XB-V3 BB-V4\n");
|
||||
for (size_t type_ss = 0; type_ss < static_cast<size_t>(EnemyType::MAX_ENEMY_TYPE); type_ss++) {
|
||||
EnemyType type = static_cast<EnemyType>(type_ss);
|
||||
for (auto type : phosg::EnumRange<EnemyType>()) {
|
||||
bool any_count_nonzero = false;
|
||||
array<size_t, NUM_VERSIONS> counts;
|
||||
for (Version v : ALL_NON_PATCH_VERSIONS) {
|
||||
@@ -3319,7 +3390,7 @@ Action a_optimize_materialized_map(
|
||||
|
||||
return false;
|
||||
};
|
||||
phosg::parallel_range_blocks<uint64_t>(thread_fn, 0, 0x100000000, 0x100, num_threads);
|
||||
phosg::parallel_blocks<uint64_t>(thread_fn, 0, 0x100000000, 0x100, num_threads);
|
||||
});
|
||||
|
||||
Action a_print_free_supermap(
|
||||
@@ -3605,7 +3676,7 @@ Action a_generate_all_dc_serial_numbers(
|
||||
found_counts[7].load(), serial_numbers[7].size(),
|
||||
found_counts[8].load(), serial_numbers[8].size());
|
||||
};
|
||||
phosg::parallel_range_blocks<uint64_t>(thread_fn, 0, 0x100000000, 0x1000, num_threads, progress_fn);
|
||||
phosg::parallel_blocks<uint64_t>(thread_fn, 0, 0x100000000, 0x1000, num_threads, progress_fn);
|
||||
|
||||
if (num_mismatches > 0) {
|
||||
throw logic_error("mismatches occurred during test");
|
||||
@@ -3749,7 +3820,7 @@ Action a_replay_ep3_battle_commands(
|
||||
run_replay(base_seed, 0);
|
||||
} else {
|
||||
size_t num_threads = args.get<size_t>("threads", 0);
|
||||
phosg::parallel_range_blocks<int64_t>(run_replay, 0, 0x100000000, 0x1000, num_threads);
|
||||
phosg::parallel_blocks<int64_t>(run_replay, 0, 0x100000000, 0x1000, num_threads);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3905,7 +3976,7 @@ Action a_run_server_replay_log(
|
||||
use_terminal_colors = true;
|
||||
}
|
||||
|
||||
auto state = make_shared<ServerState>(get_config_filename(args));
|
||||
auto state = make_shared<ServerState>(get_config_filename(args), !replay_log_filename.empty());
|
||||
if (args.get<bool>("debug")) {
|
||||
state->is_debug = true;
|
||||
}
|
||||
|
||||
+54
-12
@@ -866,9 +866,10 @@ static const vector<DATEntityDefinition> dat_object_definitions({
|
||||
// param5; if the dot product is positive, param2 is used)
|
||||
// param3 = room ID to use if the dot product described above is zero or negative
|
||||
// param5 = angle (see param2)
|
||||
// param6 = if equal to 0x00010000, only the player's room_id field is set, and game flag 0x02000000 is set on
|
||||
// the player; if not equal to 0x00010000, then both room_id and room_id2 are set and no game flag is set
|
||||
// (TODO: What are the visible behavior differences due to this parameter?)
|
||||
// param6 = on v2 and before, this is ignored; on v3 and later, if param6 is equal to 0x00010000, only the
|
||||
// player's room_id field is set, and player flag 0x02000000 is set on the player; if not equal to 0x00010000,
|
||||
// then both room_id and room_id_in_custom_area (if in a non-rearrangeable area, like Pioneer 2 or Forest) are
|
||||
// set and no game flag is set (TODO: What are the visible behavior differences due to this parameter?)
|
||||
{0x000E, F_V0_V4, 0x00005FFFFFF83FFE, "TObjRoomId"},
|
||||
|
||||
// Sensor of some kind (TODO). Params:
|
||||
@@ -1372,10 +1373,10 @@ static const vector<DATEntityDefinition> dat_object_definitions({
|
||||
|
||||
// Item box. Params:
|
||||
// param1 = if positive, box is specialized to drop a specific item or type of item; if zero or negative, box
|
||||
// drops any common item or none at all (and param3-6 are all ignored)
|
||||
// param3 = if zero, then only data1[0-1] are used and the rest of the ItemData is cleared, then bonuses, grinds,
|
||||
// etc. are applied to the item; if nonzero, the item is not randomized at all and drops exactly as specified
|
||||
// in param4-6
|
||||
// drops any item (including box rares for the current floor), or nothing at all, and param3-6 are all ignored
|
||||
// param3 = if zero, then only data1[0-1] (the high 2 bytes of param4) are used and the rest of the ItemData is
|
||||
// cleared, then bonuses, grinds, etc. are applied to the item; if nonzero, the item is not randomized at all
|
||||
// and drops exactly as specified in param4-6
|
||||
// param4-6 = item definition (see below)
|
||||
// Not all fields in ItemData can be specified in the item definition here. The field order here does not match the
|
||||
// field order in ItemData! The item definition is encoded here as follows:
|
||||
@@ -2822,7 +2823,8 @@ static const vector<DATEntityDefinition> dat_enemy_definitions({
|
||||
// param2 = if less than 1, this is a Savage Wolf; otherwise it's a Barbarous Wolf
|
||||
{0x0043, F_V0_V4, 0x0000000000600006, "TObjEneBm5Wolf"},
|
||||
|
||||
// Booma, Gobooma, or Gigobooma. Params:
|
||||
// Booma, Gobooma, or Gigobooma. The activation radius is fixed and cannot be changed: 50 for Hunters, 100 for
|
||||
// Rangers and Forces; the deactivation radius is 100 for Hunters and 150 for Rangers and Forces. Params:
|
||||
// param1 = TODO (fraction of max HP; see TObjEnemyV8048ee80_v5A)
|
||||
// param2 = idle walk radius (when there's no target, it will walk around its spawn location within this radius;
|
||||
// if this is zero, it stands still instead)
|
||||
@@ -3445,9 +3447,49 @@ void MapFile::RandomState::generate_shuffled_location_table(
|
||||
}
|
||||
|
||||
const array<uint32_t, 41> MapFile::RAND_ENEMY_BASE_TYPES = {
|
||||
0x44, 0x43, 0x41, 0x42, 0x40, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0xA0, 0xA1,
|
||||
0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xDB, 0xDC, 0xDD, 0xDE, 0xDF,
|
||||
0xE0, 0xE0, 0xE1};
|
||||
0x0044, /* TObjEneBeast */
|
||||
0x0043, /* TObjEneBm5Wolf */
|
||||
0x0041, /* TObjEneLappy */
|
||||
0x0042, /* TObjEneBm3FlyNest */
|
||||
0x0040, /* TObjEneMoja */
|
||||
0x0060, /* TObjGrass */
|
||||
0x0061, /* TObjEneRe2Flower */
|
||||
0x0062, /* TObjEneNanoDrago */
|
||||
0x0063, /* TObjEneShark */
|
||||
0x0064, /* TObjEneSlime */
|
||||
0x0065, /* TObjEnePanarms */
|
||||
0x0080, /* TObjEneDubchik */
|
||||
0x0081, /* TObjEneGyaranzo */
|
||||
0x0082, /* TObjEneMe3ShinowaReal */
|
||||
0x0083, /* TObjEneMe1Canadin */
|
||||
0x0084, /* TObjEneMe1CanadinLeader */
|
||||
0x0085, /* TOCtrlDubchik */
|
||||
0x00A0, /* TObjEneSaver */
|
||||
0x00A1, /* TObjEneRe4Sorcerer */
|
||||
0x00A2, /* TObjEneDarkGunner */
|
||||
0x00A3, /* TObjEneDarkGunCenter */
|
||||
0x00A4, /* TObjEneDf2Bringer */
|
||||
0x00A5, /* TObjEneRe7Berura */
|
||||
0x00A6, /* TObjEneDimedian */
|
||||
0x00A7, /* TObjEneBalClawBody */
|
||||
0x00A8, /* TObjEneBalClawClaw */
|
||||
0x00D4, /* TObjEneMe3StelthReal */
|
||||
0x00D5, /* TObjEneMerillLia */
|
||||
0x00D6, /* TObjEneBm9Mericarol */
|
||||
0x00D7, /* TObjEneBm5GibonU */
|
||||
0x00D8, /* TObjEneGibbles */
|
||||
0x00D9, /* TObjEneMe1Gee */
|
||||
0x00DA, /* TObjEneMe1GiGue */
|
||||
0x00DB, /* TObjEneDelDepth */
|
||||
0x00DC, /* TObjEneDellBiter */
|
||||
0x00DD, /* TObjEneDolmOlm */
|
||||
0x00DE, /* TObjEneMorfos */
|
||||
0x00DF, /* TObjEneRecobox */
|
||||
// This is not a bug; 0x00E0 really does appear twice in this list on the client.
|
||||
0x00E0, /* TObjEneMe3SinowZoaReal/TObjEneEpsilonBody (depending on area) */
|
||||
0x00E0, /* TObjEneMe3SinowZoaReal/TObjEneEpsilonBody (depending on area) */
|
||||
0x00E1, /* TObjEneIllGill */
|
||||
};
|
||||
|
||||
MapFile::MapFile(std::shared_ptr<const std::string> data) {
|
||||
for (uint8_t z = 0; z < this->sections_for_floor.size(); z++) {
|
||||
@@ -6566,7 +6608,7 @@ void MapState::import_enemy_states_from_sync(Version from_version, const SyncEne
|
||||
// Only set the state if it's not an alias
|
||||
if (ene_st->super_ene == ene) {
|
||||
if (ene_st->game_flags != entry.flags) {
|
||||
this->log.warning_f("({:04X} => E-{:03X}) Flags from client ({:08X}) do not match game flags from map ({:08X})",
|
||||
this->log.warning_f("({:04X} => E-{:03X}) Game flags from client ({:08X}) do not match game flags from map ({:08X})",
|
||||
enemy_index, ene_st->e_id, entry.flags, ene_st->game_flags);
|
||||
ene_st->game_flags = entry.flags;
|
||||
}
|
||||
|
||||
@@ -42,6 +42,12 @@ constexpr uint32_t PATCH_SWITCHES = 0x11666611;
|
||||
constexpr uint32_t PROGRAMS = 0x11777711;
|
||||
constexpr uint32_t DISCONNECT = 0x11888811;
|
||||
constexpr uint32_t CLEAR_LICENSE = 0x11999911;
|
||||
constexpr uint32_t BB_LIVE_SHIP = 0x11AAAA11;
|
||||
constexpr uint32_t BB_TEST_SHIP = 0x11BBBB11;
|
||||
constexpr uint32_t BB_VANILLA_SHIP = 0x11CCCC11;
|
||||
constexpr uint32_t BB_HARDCORE_SHIP = 0x11F00D11;
|
||||
constexpr uint32_t BB_DEV_SHIP = 0x11EEEE11;
|
||||
constexpr uint32_t BLUEBALLZ_PLUS0 = 0x11DDDD11;
|
||||
} // namespace MainMenuItemID
|
||||
|
||||
namespace ClearLicenseConfirmationMenuItemID {
|
||||
|
||||
@@ -343,11 +343,9 @@ public:
|
||||
ret.store_raw(this->value);
|
||||
return ret;
|
||||
}
|
||||
} __attribute__((packed));
|
||||
} __packed_ws_be__(ChallengeTimeT, 4);
|
||||
using ChallengeTime = ChallengeTimeT<false>;
|
||||
using ChallengeTimeBE = ChallengeTimeT<true>;
|
||||
check_struct_size(ChallengeTime, 4);
|
||||
check_struct_size(ChallengeTimeBE, 4);
|
||||
|
||||
std::string decrypt_v2_registry_value(const void* data, size_t size);
|
||||
|
||||
|
||||
@@ -76,11 +76,9 @@ struct PlayerInventoryItemT {
|
||||
bool is_equipped() const {
|
||||
return (this->flags & 8);
|
||||
}
|
||||
} __attribute__((packed));
|
||||
} __packed_ws_be__(PlayerInventoryItemT, 0x1C);
|
||||
using PlayerInventoryItem = PlayerInventoryItemT<false>;
|
||||
using PlayerInventoryItemBE = PlayerInventoryItemT<true>;
|
||||
check_struct_size(PlayerInventoryItem, 0x1C);
|
||||
check_struct_size(PlayerInventoryItemBE, 0x1C);
|
||||
|
||||
template <bool BE>
|
||||
struct PlayerBankItemT {
|
||||
@@ -100,11 +98,9 @@ struct PlayerBankItemT {
|
||||
ret.present = this->present;
|
||||
return ret;
|
||||
}
|
||||
} __attribute__((packed));
|
||||
} __packed_ws_be__(PlayerBankItemT, 0x18);
|
||||
using PlayerBankItem = PlayerBankItemT<false>;
|
||||
using PlayerBankItemBE = PlayerBankItemT<true>;
|
||||
check_struct_size(PlayerBankItem, 0x18);
|
||||
check_struct_size(PlayerBankItemBE, 0x18);
|
||||
|
||||
template <bool BE>
|
||||
struct PlayerInventoryT {
|
||||
@@ -291,11 +287,9 @@ struct PlayerInventoryT {
|
||||
ret.items = this->items;
|
||||
return ret;
|
||||
}
|
||||
} __attribute__((packed));
|
||||
} __packed_ws_be__(PlayerInventoryT, 0x34C);
|
||||
using PlayerInventory = PlayerInventoryT<false>;
|
||||
using PlayerInventoryBE = PlayerInventoryT<true>;
|
||||
check_struct_size(PlayerInventory, 0x34C);
|
||||
check_struct_size(PlayerInventoryBE, 0x34C);
|
||||
|
||||
template <size_t SlotCount, bool BE>
|
||||
struct PlayerBankT {
|
||||
|
||||
@@ -721,7 +721,7 @@ const ChallengeTemplateDefinition& get_challenge_template_definition(Version ver
|
||||
}
|
||||
|
||||
PlayerHoldState::PlayerHoldState(const PlayerHoldState_DCProtos& proto)
|
||||
: unknown_a1(proto.unknown_a1),
|
||||
: expiration_frames(proto.expiration_frames),
|
||||
unknown_a2(proto.unknown_a2),
|
||||
unknown_a3(0),
|
||||
trigger_radius2(proto.trigger_radius2),
|
||||
@@ -730,7 +730,7 @@ PlayerHoldState::PlayerHoldState(const PlayerHoldState_DCProtos& proto)
|
||||
|
||||
PlayerHoldState::operator PlayerHoldState_DCProtos() const {
|
||||
PlayerHoldState_DCProtos ret;
|
||||
ret.unknown_a1 = this->unknown_a1;
|
||||
ret.expiration_frames = this->expiration_frames;
|
||||
ret.unknown_a2 = this->unknown_a2;
|
||||
ret.trigger_radius2 = this->trigger_radius2;
|
||||
ret.x = this->x;
|
||||
|
||||
+15
-22
@@ -64,6 +64,12 @@ struct PlayerVisualConfigT {
|
||||
// F = force, R = ranger, H = hunter
|
||||
// A = android, N = newman, M = human
|
||||
// f = female, m = male
|
||||
// Enemies also have a class_flags field, though it isn't part of PlayerVisualConfig. The bits for enemies are:
|
||||
// -------- -------- -------- ----DMAN
|
||||
// D = Dark attribute
|
||||
// M = Machine attribute
|
||||
// A = Altered Beast attribute
|
||||
// N = Native attribute
|
||||
/* 34 */ U32T<BE> class_flags = 0;
|
||||
/* 38 */ U16T<BE> costume = 0;
|
||||
/* 3A */ U16T<BE> skin = 0;
|
||||
@@ -306,11 +312,9 @@ struct PlayerVisualConfigT {
|
||||
ret.proportion_y = this->proportion_y;
|
||||
return ret;
|
||||
}
|
||||
} __attribute__((packed));
|
||||
} __packed_ws_be__(PlayerVisualConfigT, 0x50);
|
||||
using PlayerVisualConfig = PlayerVisualConfigT<false>;
|
||||
using PlayerVisualConfigBE = PlayerVisualConfigT<true>;
|
||||
check_struct_size(PlayerVisualConfig, 0x50);
|
||||
check_struct_size(PlayerVisualConfigBE, 0x50);
|
||||
|
||||
template <bool BE>
|
||||
struct PlayerDispDataDCPCV3T {
|
||||
@@ -325,11 +329,9 @@ struct PlayerDispDataDCPCV3T {
|
||||
}
|
||||
|
||||
PlayerDispDataBB to_bb(Language to_language, Language from_language) const;
|
||||
} __attribute__((packed));
|
||||
} __packed_ws_be__(PlayerDispDataDCPCV3T, 0xD0);
|
||||
using PlayerDispDataDCPCV3 = PlayerDispDataDCPCV3T<false>;
|
||||
using PlayerDispDataDCPCV3BE = PlayerDispDataDCPCV3T<true>;
|
||||
check_struct_size(PlayerDispDataDCPCV3, 0xD0);
|
||||
check_struct_size(PlayerDispDataDCPCV3BE, 0xD0);
|
||||
|
||||
struct PlayerDispDataBBPreview {
|
||||
/* 00 */ le_uint32_t experience = 0;
|
||||
@@ -592,11 +594,9 @@ struct ChallengeAwardStateT {
|
||||
ret.maximum_rank = this->maximum_rank;
|
||||
return ret;
|
||||
}
|
||||
} __attribute__((packed));
|
||||
} __packed_ws_be__(ChallengeAwardStateT, 8);
|
||||
using ChallengeAwardState = ChallengeAwardStateT<false>;
|
||||
using ChallengeAwardStateBE = ChallengeAwardStateT<true>;
|
||||
check_struct_size(ChallengeAwardState, 8);
|
||||
check_struct_size(ChallengeAwardStateBE, 8);
|
||||
|
||||
template <TextEncoding UnencryptedEncoding, TextEncoding EncryptedEncoding>
|
||||
struct PlayerRecordsChallengeDCPCT {
|
||||
@@ -667,11 +667,9 @@ struct PlayerRecordsChallengeV3T {
|
||||
/* 00D8:00F4 */ pstring<TextEncoding::CHALLENGE8, 0x0C> rank_title;
|
||||
/* 00E4:0100 */ parray<uint8_t, 0x1C> unknown_l7;
|
||||
/* 0100:011C */
|
||||
} __attribute__((packed));
|
||||
} __packed_ws_be__(PlayerRecordsChallengeV3T, 0x100);
|
||||
using PlayerRecordsChallengeV3 = PlayerRecordsChallengeV3T<false>;
|
||||
using PlayerRecordsChallengeV3BE = PlayerRecordsChallengeV3T<true>;
|
||||
check_struct_size(PlayerRecordsChallengeV3, 0x100);
|
||||
check_struct_size(PlayerRecordsChallengeV3BE, 0x100);
|
||||
|
||||
struct PlayerRecordsChallengeEp3 {
|
||||
/* 00:1C */ be_uint16_t title_color = 0x7FFF; // XRGB1555
|
||||
@@ -698,8 +696,7 @@ struct PlayerRecordsChallengeEp3 {
|
||||
/* C8:E4 */ ChallengeAwardStateT<true> ep2_online_award_state;
|
||||
/* D0:EC */ ChallengeAwardStateT<true> ep1_offline_award_state;
|
||||
/* D8:F4 */
|
||||
} __attribute__((packed));
|
||||
check_struct_size(PlayerRecordsChallengeEp3, 0xD8);
|
||||
} __packed_ws__(PlayerRecordsChallengeEp3, 0xD8);
|
||||
|
||||
struct PlayerRecordsChallengeBB {
|
||||
/* 0000 */ le_uint16_t title_color = 0x7FFF; // XRGB1555
|
||||
@@ -827,11 +824,9 @@ struct PlayerRecordsBattleT {
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
} __attribute__((packed));
|
||||
} __packed_ws_be__(PlayerRecordsBattleT, 0x18);
|
||||
using PlayerRecordsBattle = PlayerRecordsBattleT<false>;
|
||||
using PlayerRecordsBattleBE = PlayerRecordsBattleT<true>;
|
||||
check_struct_size(PlayerRecordsBattle, 0x18);
|
||||
check_struct_size(PlayerRecordsBattleBE, 0x18);
|
||||
|
||||
template <typename DestT, typename SrcT = DestT>
|
||||
DestT convert_player_disp_data(const SrcT&, Language, Language) {
|
||||
@@ -1106,11 +1101,9 @@ struct SymbolChatT {
|
||||
ret.face_parts = this->face_parts;
|
||||
return ret;
|
||||
}
|
||||
} __attribute__((packed));
|
||||
} __packed_ws_be__(SymbolChatT, 0x3C);
|
||||
using SymbolChat = SymbolChatT<false>;
|
||||
using SymbolChatBE = SymbolChatT<true>;
|
||||
check_struct_size(SymbolChat, 0x3C);
|
||||
check_struct_size(SymbolChatBE, 0x3C);
|
||||
|
||||
struct TelepipeState {
|
||||
/* 00 */ le_uint16_t owner_client_id = 0xFFFF;
|
||||
@@ -1123,7 +1116,7 @@ struct TelepipeState {
|
||||
|
||||
struct PlayerHoldState_DCProtos {
|
||||
// This is used in all versions of this command except DCNTE and 11/2000.
|
||||
/* 00 */ le_uint16_t unknown_a1 = 0;
|
||||
/* 00 */ le_uint16_t expiration_frames = 0;
|
||||
/* 02 */ le_uint16_t unknown_a2 = 0;
|
||||
// unknown_a3 is missing in this format, unlike the v1+ format below
|
||||
/* 04 */ le_float trigger_radius2 = 0.0f;
|
||||
@@ -1134,7 +1127,7 @@ struct PlayerHoldState_DCProtos {
|
||||
|
||||
struct PlayerHoldState {
|
||||
// This is used in all versions of this command except DCNTE and 11/2000.
|
||||
/* 00 */ le_uint16_t unknown_a1 = 0;
|
||||
/* 00 */ le_uint16_t expiration_frames = 0;
|
||||
/* 02 */ le_uint16_t unknown_a2 = 0;
|
||||
/* 04 */ le_uint32_t unknown_a3 = 0;
|
||||
/* 08 */ le_float trigger_radius2 = 0.0f;
|
||||
|
||||
+28
-6
@@ -74,7 +74,14 @@ static asio::awaitable<HandlerResult> C_1D(shared_ptr<Client> c, Channel::Messag
|
||||
c->ping_start_time = 0;
|
||||
double ping_ms = static_cast<double>(ping_usecs) / 1000.0;
|
||||
send_text_message_fmt(c->channel, "To proxy: {:g}ms", ping_ms);
|
||||
co_return HandlerResult::SUPPRESS;
|
||||
}
|
||||
|
||||
if (c->proxy_session->is_in_game) {
|
||||
c->log.info_f("Forwarding in-game command 1D through proxy");
|
||||
co_return HandlerResult::FORWARD;
|
||||
}
|
||||
|
||||
co_return HandlerResult::SUPPRESS;
|
||||
}
|
||||
|
||||
@@ -960,7 +967,8 @@ static asio::awaitable<HandlerResult> SC_6x60_6xA2(shared_ptr<Client> c, Channel
|
||||
if (rec.obj_st) {
|
||||
if (rec.ignore_def) {
|
||||
c->log.info_f("Creating item from box {:04X} (area {:02X})", cmd.entity_index, cmd.effective_area);
|
||||
res = c->proxy_session->item_creator->on_box_item_drop(cmd.effective_area);
|
||||
res = c->proxy_session->item_creator->on_box_item_drop(
|
||||
cmd.effective_area, c->check_flag(Client::Flag::ALL_RARES_ENABLED));
|
||||
} else {
|
||||
c->log.info_f("Creating item from box {:04X} (area {:02X}; specialized with {:g} {:08X} {:08X} {:08X})",
|
||||
cmd.entity_index, cmd.effective_area,
|
||||
@@ -970,7 +978,8 @@ static asio::awaitable<HandlerResult> SC_6x60_6xA2(shared_ptr<Client> c, Channel
|
||||
}
|
||||
} else {
|
||||
c->log.info_f("Creating item from enemy {:04X} (area {:02X})", cmd.entity_index, cmd.effective_area);
|
||||
res = c->proxy_session->item_creator->on_monster_item_drop(rec.effective_rt_index, cmd.effective_area);
|
||||
res = c->proxy_session->item_creator->on_monster_item_drop(
|
||||
rec.effective_enemy_type, cmd.effective_area, c->check_flag(Client::Flag::ALL_RARES_ENABLED));
|
||||
}
|
||||
|
||||
if (res.item.empty()) {
|
||||
@@ -1022,8 +1031,15 @@ static asio::awaitable<HandlerResult> S_6x(shared_ptr<Client> c, Channel::Messag
|
||||
case 0x17: {
|
||||
const auto& cmd = msg.check_size_t<G_SetEntityPositionAndAngle_6x17>();
|
||||
if (cmd.header.entity_id == c->lobby_client_id) {
|
||||
c->log.warning_f("Blocking subcommand 6x17 targeting local client");
|
||||
co_return HandlerResult::SUPPRESS;
|
||||
// Vol Opt phase 1 -> phase 2 uses 6x17 targeting the local client to move
|
||||
// players into the second arena phase. Allow this only while the proxy-side
|
||||
// client is already on the Vol Opt floor.
|
||||
if (c->floor == 0x0D) {
|
||||
c->log.info_f("Allowing subcommand 6x17 targeting local client on Vol Opt floor");
|
||||
} else {
|
||||
c->log.warning_f("Blocking subcommand 6x17 targeting local client outside Vol Opt floor");
|
||||
co_return HandlerResult::SUPPRESS;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -1643,7 +1659,7 @@ static asio::awaitable<HandlerResult> S_65_67_68_EB(shared_ptr<Client> c, Channe
|
||||
c->log.warning_f("Proxied player appears multiple times in lobby");
|
||||
}
|
||||
|
||||
if constexpr (sizeof(cmd.lobby_flags) > sizeof(LobbyFlags_DCNTE)) {
|
||||
if constexpr (sizeof(cmd.lobby_flags) > sizeof(LobbyFlagsDCNTE)) {
|
||||
c->proxy_session->lobby_event = cmd.lobby_flags.event;
|
||||
if (c->override_lobby_event != 0xFF) {
|
||||
cmd.lobby_flags.event = c->override_lobby_event;
|
||||
@@ -2214,6 +2230,7 @@ static asio::awaitable<HandlerResult> C_V123_A0(shared_ptr<Client> c, Channel::M
|
||||
// Change Ship from the lobby counter menu. We override the Change Ship action to end the proxy session, but we only
|
||||
// do so if the player is in a lobby in order to properly handle the download quest case.
|
||||
if (c->proxy_session->is_in_lobby) {
|
||||
c->proxy_session->ending_intentionally = true;
|
||||
c->proxy_session->server_channel->disconnect();
|
||||
co_return HandlerResult::SUPPRESS;
|
||||
} else {
|
||||
@@ -2828,7 +2845,12 @@ asio::awaitable<void> handle_proxy_server_commands(
|
||||
if (ec == asio::error::eof || ec == asio::error::connection_reset) {
|
||||
error_str = "Server channel\ndisconnected";
|
||||
} else if (ec == asio::error::operation_aborted) {
|
||||
// This happens when the player chooses Change Ship/Change Block, so we don't show an error message
|
||||
// If this is the currently-active backend channel, treat the abort as a backend disconnect.
|
||||
// Normal Change Ship/Change Block reconnects replace ses->server_channel first; the old
|
||||
// aborted channel will not match here, so those expected aborts stay silent.
|
||||
if ((c->proxy_session == ses) && (ses->server_channel == channel) && !ses->ending_intentionally) {
|
||||
error_str = "Server channel\ndisconnected";
|
||||
}
|
||||
} else {
|
||||
error_str = e.what();
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
struct ServerState;
|
||||
|
||||
struct ProxySession {
|
||||
bool ending_intentionally = false;
|
||||
static size_t num_proxy_sessions;
|
||||
|
||||
std::shared_ptr<Channel> server_channel;
|
||||
|
||||
+2
-4
@@ -51,11 +51,9 @@ struct PSOMemCardDLQFileEncryptedHeaderT {
|
||||
le_uint32_t decompressed_size;
|
||||
le_uint32_t round3_seed;
|
||||
// Data follows here.
|
||||
} __attribute__((packed));
|
||||
} __packed_ws_be__(PSOMemCardDLQFileEncryptedHeaderT, 0x10);
|
||||
using PSOVMSDLQFileEncryptedHeader = PSOMemCardDLQFileEncryptedHeaderT<false>;
|
||||
using PSOGCIDLQFileEncryptedHeader = PSOMemCardDLQFileEncryptedHeaderT<true>;
|
||||
check_struct_size(PSOVMSDLQFileEncryptedHeader, 0x10);
|
||||
check_struct_size(PSOGCIDLQFileEncryptedHeader, 0x10);
|
||||
|
||||
template <bool BE>
|
||||
string decrypt_download_quest_data_section(
|
||||
@@ -154,7 +152,7 @@ string find_seed_and_decrypt_download_quest_data_section(
|
||||
const void* data_section, size_t size, bool skip_checksum, bool is_ep3_trial, size_t num_threads) {
|
||||
mutex result_lock;
|
||||
string result;
|
||||
uint64_t result_seed = phosg::parallel_range_blocks<uint64_t>([&](uint64_t seed, size_t) {
|
||||
uint64_t result_seed = phosg::parallel_blocks<uint64_t>([&](uint64_t seed, size_t) {
|
||||
try {
|
||||
string ret = decrypt_download_quest_data_section<BE>(data_section, size, seed, skip_checksum, is_ep3_trial);
|
||||
lock_guard<mutex> g(result_lock);
|
||||
|
||||
+8
-6
@@ -676,7 +676,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = {
|
||||
// Sets a player's starting position.
|
||||
// valueA = client ID
|
||||
// regsB[0-2] = position (x, y, z as integers)
|
||||
// r egsB[3] = angle
|
||||
// regsB[3] = angle
|
||||
{0x76, "set_player_start_position", "p_setpos", {CLIENT_ID, {R_REG_SET_FIXED, 4}}, F_V0_V4 | F_ARGS},
|
||||
|
||||
// Returns players to the Hunter's Guild counter.
|
||||
@@ -780,6 +780,8 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = {
|
||||
// regsA[0-2] = location (x, y, z as integers)
|
||||
// regsA[3] = radius
|
||||
// regsA[4] = label index where thread should start
|
||||
// After executing this opcode, regsA[0] is replaced with the object's token, which can be used in del_obj_param,
|
||||
// move_coords_object, etc.
|
||||
{0x8C, "at_coords_call", nullptr, {{R_REG_SET_FIXED, 5}}, F_V0_V4},
|
||||
|
||||
// Like at_coords_call, but the thread is not started automatically. Instead, the player's primary action button
|
||||
@@ -890,7 +892,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = {
|
||||
{0xB1, "thread_stg", nullptr, {SCRIPT16}, F_V0_V4},
|
||||
|
||||
// Deletes an interactable object previously created by set_obj_param. valueA is the object's token, as returned by
|
||||
// regB from set_obj_param.
|
||||
// regB from set_obj_param, or regsA[0] from e.g. at_coords_call.
|
||||
{0xB2, "del_obj_param", nullptr, {R_REG}, F_V0_V4},
|
||||
|
||||
// Creates an item in the player's inventory. If the item is successfully created, this opcode sends 6x2B on all
|
||||
@@ -2004,7 +2006,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = {
|
||||
{0xF8CB, "clear_slot_invincible", "set_slot_targetable?", {R_REG}, F_V3_V4},
|
||||
|
||||
// These opcodes inflict various status conditions on a player. In the case of Shifta/Deband/Jellen/Zalure, the
|
||||
// effective technicuqe level is 21.
|
||||
// effective technique level is 21.
|
||||
// regA = client ID
|
||||
{0xF8CC, "set_slot_poison", nullptr, {R_REG}, F_V3_V4},
|
||||
{0xF8CD, "set_slot_paralyze", nullptr, {R_REG}, F_V3_V4},
|
||||
@@ -2089,9 +2091,9 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = {
|
||||
// regsB[0-2] = location (x, y, z as integers)
|
||||
{0xF8E6, "move_coords_object", nullptr, {R_REG, {R_REG_SET_FIXED, 3}}, F_V3_V4},
|
||||
|
||||
// These are the same as their counterparts without _ex, but these return an object token in regB which can be used
|
||||
// with del_obj_param, move_coords_object, etc. set_obj_param_ex is the same as set_obj_param, since set_obj_param
|
||||
// already returns an object token.
|
||||
// These are the same as their counterparts without _ex, but these also return the object ID (which is distinct
|
||||
// from the object token) in regB. There is a bug in set_obj_param_ex - the object token is not returned anywhere,
|
||||
// and only the object ID is returned in regB.
|
||||
{0xF8E7, "at_coords_call_ex", nullptr, {{R_REG_SET_FIXED, 5}, W_REG}, F_V3_V4},
|
||||
{0xF8E8, "at_coords_talk_ex", nullptr, {{R_REG_SET_FIXED, 5}, W_REG}, F_V3_V4},
|
||||
{0xF8E9, "npc_coords_call_ex", "walk_to_coord_call_ex", {{R_REG_SET_FIXED, 5}, W_REG}, F_V3_V4},
|
||||
|
||||
+172
-89
@@ -177,23 +177,31 @@ RareItemSet::ParsedRELData::ParsedRELData(phosg::StringReader r, bool big_endian
|
||||
}
|
||||
|
||||
RareItemSet::ParsedRELData::ParsedRELData(const SpecCollection& collection) {
|
||||
for (const auto& specs : collection.rt_index_to_specs) {
|
||||
ExpandedDrop effective_spec;
|
||||
this->monster_rares.resize(NUM_RT_INDEXES_V4);
|
||||
|
||||
for (const auto& [enemy_type, specs] : collection.enemy_specs) {
|
||||
const auto& def = type_definition_for_enemy(enemy_type);
|
||||
if (def.rt_index == 0xFF) {
|
||||
throw runtime_error(std::format(
|
||||
"monster spec for {} has no rt_index and cannot be converted to ItemRT format", def.enum_name));
|
||||
}
|
||||
|
||||
auto& dest_spec = this->monster_rares.at(def.rt_index);
|
||||
for (const auto& spec : specs) {
|
||||
if (effective_spec.data.empty()) {
|
||||
effective_spec = spec;
|
||||
} else if ((effective_spec.probability != spec.probability) || (effective_spec.data != spec.data)) {
|
||||
throw runtime_error("monster spec cannot be converted to ItemRT format");
|
||||
if (dest_spec.data.empty()) {
|
||||
dest_spec = spec;
|
||||
} else if ((dest_spec.probability != spec.probability) || (dest_spec.data != spec.data)) {
|
||||
throw runtime_error(std::format(
|
||||
"monster spec for {} contains multiple drops and cannot be converted to ItemRT format", def.enum_name));
|
||||
}
|
||||
}
|
||||
this->monster_rares.emplace_back(specs.empty() ? ExpandedDrop() : specs[0]);
|
||||
}
|
||||
|
||||
if (collection.box_area_norm_to_specs.size() > 0xFF) {
|
||||
if (collection.box_specs.size() > 0xFF) {
|
||||
throw runtime_error("area_norm value too high");
|
||||
}
|
||||
for (uint8_t area_norm = 0; area_norm < collection.box_area_norm_to_specs.size(); area_norm++) {
|
||||
for (const auto& spec : collection.box_area_norm_to_specs[area_norm]) {
|
||||
for (uint8_t area_norm = 0; area_norm < collection.box_specs.size(); area_norm++) {
|
||||
for (const auto& spec : collection.box_specs[area_norm]) {
|
||||
uint8_t area_norm_plus_1 = area_norm + 1;
|
||||
this->box_rares.emplace_back(BoxRare{.area_norm_plus_1 = area_norm_plus_1, .drop = spec});
|
||||
}
|
||||
@@ -208,27 +216,26 @@ std::string RareItemSet::ParsedRELData::serialize(bool big_endian, bool is_v1) c
|
||||
}
|
||||
}
|
||||
|
||||
RareItemSet::SpecCollection RareItemSet::ParsedRELData::as_collection() const {
|
||||
RareItemSet::SpecCollection RareItemSet::ParsedRELData::as_collection(Episode episode) const {
|
||||
SpecCollection ret;
|
||||
for (size_t z = 0; z < this->monster_rares.size(); z++) {
|
||||
const auto& drop = this->monster_rares[z];
|
||||
for (size_t rt_index = 0; rt_index < this->monster_rares.size(); rt_index++) {
|
||||
const auto& drop = this->monster_rares[rt_index];
|
||||
if (drop.data.empty()) {
|
||||
continue;
|
||||
}
|
||||
if (z >= ret.rt_index_to_specs.size()) {
|
||||
ret.rt_index_to_specs.resize(z + 1);
|
||||
for (auto enemy_type : enemy_types_for_rare_table_index(episode, rt_index)) {
|
||||
ret.enemy_specs[enemy_type].emplace_back(drop);
|
||||
}
|
||||
ret.rt_index_to_specs[z].emplace_back(drop);
|
||||
}
|
||||
for (const auto& drop : this->box_rares) {
|
||||
if ((drop.area_norm_plus_1 == 0) || drop.drop.data.empty()) {
|
||||
continue;
|
||||
}
|
||||
uint8_t area_norm = drop.area_norm_plus_1 - 1;
|
||||
if (area_norm >= ret.box_area_norm_to_specs.size()) {
|
||||
ret.box_area_norm_to_specs.resize(area_norm + 1);
|
||||
if (area_norm >= ret.box_specs.size()) {
|
||||
ret.box_specs.resize(area_norm + 1);
|
||||
}
|
||||
ret.box_area_norm_to_specs[area_norm].emplace_back(drop.drop);
|
||||
ret.box_specs[area_norm].emplace_back(drop.drop);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
@@ -241,8 +248,7 @@ RareItemSet::RareItemSet(const AFSArchive& afs, bool is_v1) {
|
||||
size_t index = static_cast<size_t>(difficulty) * 10 + section_id;
|
||||
ParsedRELData rel(afs.get_reader(index), false, is_v1);
|
||||
this->collections.emplace(
|
||||
this->key_for_params(mode, Episode::EP1, difficulty, section_id),
|
||||
rel.as_collection());
|
||||
this->key_for_params(mode, Episode::EP1, difficulty, section_id), rel.as_collection(Episode::EP1));
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
}
|
||||
@@ -250,7 +256,8 @@ RareItemSet::RareItemSet(const AFSArchive& afs, bool is_v1) {
|
||||
}
|
||||
}
|
||||
|
||||
string RareItemSet::gsl_entry_name_for_table(GameMode mode, Episode episode, Difficulty difficulty, uint8_t section_id) {
|
||||
string RareItemSet::gsl_entry_name_for_table(
|
||||
GameMode mode, Episode episode, Difficulty difficulty, uint8_t section_id) {
|
||||
return std::format("ItemRT{}{}{}{}.rel",
|
||||
((mode == GameMode::CHALLENGE) ? "c" : ""),
|
||||
((episode == Episode::EP2) ? "l" : ""),
|
||||
@@ -259,7 +266,7 @@ string RareItemSet::gsl_entry_name_for_table(GameMode mode, Episode episode, Dif
|
||||
}
|
||||
|
||||
RareItemSet::RareItemSet(const GSLArchive& gsl, bool is_big_endian) {
|
||||
for (GameMode mode : ALL_GAME_MODES_V23) {
|
||||
for (GameMode mode : {GameMode::NORMAL, GameMode::CHALLENGE}) {
|
||||
for (Episode episode : ALL_EPISODES_V3) {
|
||||
for (Difficulty difficulty : ALL_DIFFICULTIES_V234) {
|
||||
for (size_t section_id = 0; section_id < 10; section_id++) {
|
||||
@@ -267,7 +274,7 @@ RareItemSet::RareItemSet(const GSLArchive& gsl, bool is_big_endian) {
|
||||
string filename = this->gsl_entry_name_for_table(mode, episode, difficulty, section_id);
|
||||
ParsedRELData rel(gsl.get_reader(filename), is_big_endian, false);
|
||||
this->collections.emplace(
|
||||
this->key_for_params(mode, episode, difficulty, section_id), rel.as_collection());
|
||||
this->key_for_params(mode, episode, difficulty, section_id), rel.as_collection(episode));
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
}
|
||||
@@ -287,7 +294,7 @@ RareItemSet::RareItemSet(const string& rel_data, bool is_big_endian) {
|
||||
size_t index = (ep_index * 40) + static_cast<size_t>(difficulty) * 10 + section_id;
|
||||
ParsedRELData rel(r.sub(0x280 * index, 0x280), is_big_endian, false);
|
||||
this->collections.emplace(
|
||||
this->key_for_params(GameMode::NORMAL, episode, difficulty, section_id), rel.as_collection());
|
||||
this->key_for_params(GameMode::NORMAL, episode, difficulty, section_id), rel.as_collection(episode));
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
}
|
||||
@@ -315,26 +322,19 @@ RareItemSet::RareItemSet(const phosg::JSON& json, shared_ptr<const ItemNameIndex
|
||||
uint8_t section_id = section_id_for_name(section_id_it.first);
|
||||
|
||||
auto& collection = this->collections[this->key_for_params(mode, episode, difficulty, section_id)];
|
||||
for (const auto& item_it : section_id_it.second->as_dict()) {
|
||||
for (const auto& [enemy_type_name, specs_json] : section_id_it.second->as_dict()) {
|
||||
vector<ExpandedDrop>* target;
|
||||
if (item_it.first.starts_with("Box-")) {
|
||||
uint8_t area_norm = FloorDefinition::get(episode, item_it.first.substr(4)).drop_area_norm;
|
||||
if (collection.box_area_norm_to_specs.size() <= area_norm) {
|
||||
collection.box_area_norm_to_specs.resize(area_norm + 1);
|
||||
if (enemy_type_name.starts_with("Box-")) {
|
||||
uint8_t area_norm = FloorDefinition::get(episode, enemy_type_name.substr(4)).drop_area_norm;
|
||||
if (collection.box_specs.size() <= area_norm) {
|
||||
collection.box_specs.resize(area_norm + 1);
|
||||
}
|
||||
target = &collection.box_area_norm_to_specs[area_norm];
|
||||
target = &collection.box_specs[area_norm];
|
||||
} else {
|
||||
size_t rt_index = type_definition_for_enemy(phosg::enum_for_name<EnemyType>(item_it.first)).rt_index;
|
||||
if (rt_index == 0xFF) {
|
||||
throw runtime_error("enemy type " + item_it.first + " does not have an rt_index");
|
||||
}
|
||||
if (collection.rt_index_to_specs.size() <= rt_index) {
|
||||
collection.rt_index_to_specs.resize(rt_index + 1);
|
||||
}
|
||||
target = &collection.rt_index_to_specs[rt_index];
|
||||
target = &collection.enemy_specs[phosg::enum_for_name<EnemyType>(enemy_type_name)];
|
||||
}
|
||||
|
||||
for (const auto& spec_json : item_it.second->as_list()) {
|
||||
for (const auto& spec_json : specs_json->as_list()) {
|
||||
auto& d = target->emplace_back();
|
||||
|
||||
auto prob_desc = spec_json->at(0);
|
||||
@@ -702,7 +702,11 @@ string RareItemSet::serialize_html(
|
||||
std::string exact_token = std::format("Exact rate: {} / {}", frac.first, frac.second);
|
||||
if (common_item_set && type_def && type_def->rt_index != 0xFF) {
|
||||
auto table = common_item_set->get_table(episode, mode, difficulty, section_id);
|
||||
uint8_t dar = table->enemy_type_drop_probs.at(type_def->rt_index);
|
||||
uint8_t dar = 0;
|
||||
try {
|
||||
dar = table->enemy_type_drop_probs.at(type_def->type);
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
exact_token += std::format(" (DAR: {}%)", dar);
|
||||
frac.first *= dar;
|
||||
frac.second *= 100;
|
||||
@@ -747,13 +751,9 @@ string RareItemSet::serialize_html(
|
||||
for (const auto& zone_type : zone_types) {
|
||||
add_location_header(zone_type.name);
|
||||
for (EnemyType type : zone_type.types) {
|
||||
uint8_t rt_index = type_definition_for_enemy(type).rt_index;
|
||||
if (rt_index == 0xFF) {
|
||||
continue;
|
||||
}
|
||||
array<vector<ExpandedDrop>, 10> specs_lists;
|
||||
for (uint8_t section_id = 0; section_id < 10; section_id++) {
|
||||
specs_lists[section_id] = this->get_enemy_specs(mode, episode, difficulty, section_id, rt_index);
|
||||
specs_lists[section_id] = this->get_enemy_specs(mode, episode, difficulty, section_id, type);
|
||||
}
|
||||
const auto& type_def = type_definition_for_enemy(type);
|
||||
const char* name = (difficulty == Difficulty::ULTIMATE && type_def.ultimate_name) ? type_def.ultimate_name : type_def.in_game_name;
|
||||
@@ -787,13 +787,9 @@ phosg::JSON RareItemSet::json(shared_ptr<const ItemNameIndex> name_index) const
|
||||
auto section_id_dict = phosg::JSON::dict();
|
||||
for (uint8_t section_id = 0; section_id < 10; section_id++) {
|
||||
auto collection_dict = phosg::JSON::dict();
|
||||
for (size_t rt_index = 0; rt_index < 0x80; rt_index++) {
|
||||
const auto& enemy_types = enemy_types_for_rare_table_index(episode, rt_index);
|
||||
if (enemy_types.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const auto& spec : this->get_enemy_specs(GameMode::NORMAL, episode, difficulty, section_id, rt_index)) {
|
||||
for (auto enemy_type : phosg::EnumRange<EnemyType>()) {
|
||||
const auto& specs = this->get_enemy_specs(GameMode::NORMAL, episode, difficulty, section_id, enemy_type);
|
||||
for (const auto& spec : specs) {
|
||||
if (spec.data.empty()) {
|
||||
continue;
|
||||
}
|
||||
@@ -807,12 +803,8 @@ phosg::JSON RareItemSet::json(shared_ptr<const ItemNameIndex> name_index) const
|
||||
if (name_index) {
|
||||
spec_json.emplace_back(name_index->describe_item(spec.data));
|
||||
}
|
||||
for (const auto& enemy_type : enemy_types) {
|
||||
if (type_definition_for_enemy(enemy_type).valid_in_episode(episode)) {
|
||||
phosg::JSON this_spec_json = spec_json;
|
||||
collection_dict.emplace(phosg::name_for_enum(enemy_type), phosg::JSON::list()).first->second->emplace_back(std::move(this_spec_json));
|
||||
}
|
||||
}
|
||||
auto list_emplace_ret = collection_dict.emplace(phosg::name_for_enum(enemy_type), phosg::JSON::list());
|
||||
list_emplace_ret.first->second->emplace_back(std::move(spec_json));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -858,17 +850,17 @@ phosg::JSON RareItemSet::json(shared_ptr<const ItemNameIndex> name_index) const
|
||||
}
|
||||
|
||||
void RareItemSet::multiply_all_rates(double factor) {
|
||||
auto multiply_rates_vec = +[](vector<vector<ExpandedDrop>>& vec, double factor) -> void {
|
||||
for (auto& vec_it : vec) {
|
||||
for (auto& z_it : vec_it) {
|
||||
uint64_t new_probability = z_it.probability * factor;
|
||||
z_it.probability = min<uint64_t>(new_probability, 0xFFFFFFFF);
|
||||
for (auto& [_, collection] : this->collections) {
|
||||
for (auto& [_, specs] : collection.enemy_specs) {
|
||||
for (auto& spec : specs) {
|
||||
spec.probability = min<uint64_t>(spec.probability * factor, 0xFFFFFFFF);
|
||||
}
|
||||
}
|
||||
for (auto& specs : collection.box_specs) {
|
||||
for (auto& spec : specs) {
|
||||
spec.probability = min<uint64_t>(spec.probability * factor, 0xFFFFFFFF);
|
||||
}
|
||||
}
|
||||
};
|
||||
for (auto& coll_it : this->collections) {
|
||||
multiply_rates_vec(coll_it.second.rt_index_to_specs, factor);
|
||||
multiply_rates_vec(coll_it.second.box_area_norm_to_specs, factor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -893,28 +885,22 @@ void RareItemSet::print_collection(
|
||||
name_for_section_id(section_id));
|
||||
|
||||
phosg::fwrite_fmt(stream, " Monster rares:\n");
|
||||
for (size_t z = 0; z < collection->rt_index_to_specs.size(); z++) {
|
||||
string enemy_types_str;
|
||||
const auto& enemy_types = enemy_types_for_rare_table_index(episode, z);
|
||||
for (EnemyType enemy_type : enemy_types) {
|
||||
enemy_types_str += phosg::name_for_enum(enemy_type);
|
||||
enemy_types_str.push_back(',');
|
||||
}
|
||||
if (!enemy_types_str.empty()) {
|
||||
enemy_types_str.resize(enemy_types_str.size() - 1);
|
||||
}
|
||||
|
||||
for (const auto& spec : collection->rt_index_to_specs[z]) {
|
||||
string s = name_index ? spec.str(name_index) : spec.str();
|
||||
phosg::fwrite_fmt(stream, " {:02X}: {} ({})\n", z, s, enemy_types_str);
|
||||
for (auto enemy_type : phosg::EnumRange<EnemyType>()) {
|
||||
try {
|
||||
const auto& def = type_definition_for_enemy(enemy_type);
|
||||
for (const auto& spec : collection->enemy_specs.at(enemy_type)) {
|
||||
string s = name_index ? spec.str(name_index) : spec.str();
|
||||
phosg::fwrite_fmt(stream, " {:<23} {}\n", def.enum_name, s);
|
||||
}
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
}
|
||||
|
||||
phosg::fwrite_fmt(stream, " Box rares:\n");
|
||||
for (size_t area_norm = 0; area_norm < collection->box_area_norm_to_specs.size(); area_norm++) {
|
||||
for (const auto& spec : collection->box_area_norm_to_specs[area_norm]) {
|
||||
for (size_t area_norm = 0; area_norm < collection->box_specs.size(); area_norm++) {
|
||||
for (const auto& spec : collection->box_specs[area_norm]) {
|
||||
string s = name_index ? spec.str(name_index) : spec.str();
|
||||
phosg::fwrite_fmt(stream, " (area-norm {:02X}) {}\n", area_norm, s);
|
||||
phosg::fwrite_fmt(stream, " (area-norm {:02X}) {}\n", area_norm, s);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -934,10 +920,100 @@ void RareItemSet::print_all_collections(FILE* stream, std::shared_ptr<const Item
|
||||
}
|
||||
}
|
||||
|
||||
void RareItemSet::SpecCollection::print_diff(FILE* stream, const SpecCollection& other) const {
|
||||
auto format_specs = [](const std::vector<ExpandedDrop>& specs) -> std::string {
|
||||
std::string ret;
|
||||
for (const auto& spec : specs) {
|
||||
if (!ret.empty()) {
|
||||
ret += ",";
|
||||
}
|
||||
ret += std::format("{:08X}:{}", spec.probability, spec.data.short_hex());
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
|
||||
const std::vector<ExpandedDrop> empty_specs{};
|
||||
for (auto enemy_type : phosg::EnumRange<EnemyType>()) {
|
||||
const std::vector<ExpandedDrop>* this_specs = &empty_specs;
|
||||
const std::vector<ExpandedDrop>* other_specs = &empty_specs;
|
||||
try {
|
||||
this_specs = &this->enemy_specs.at(enemy_type);
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
try {
|
||||
other_specs = &other.enemy_specs.at(enemy_type);
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
if (*this_specs != *other_specs) {
|
||||
phosg::fwrite_fmt(stream, " {}: {} -> {}\n",
|
||||
phosg::name_for_enum(enemy_type), format_specs(*this_specs), format_specs(*other_specs));
|
||||
}
|
||||
}
|
||||
for (size_t area_norm = 0; area_norm < 10; area_norm++) {
|
||||
const auto& this_specs = (area_norm < this->box_specs.size()) ? this->box_specs[area_norm] : empty_specs;
|
||||
const auto& other_specs = (area_norm < other.box_specs.size()) ? other.box_specs[area_norm] : empty_specs;
|
||||
if (this_specs != other_specs) {
|
||||
phosg::fwrite_fmt(stream, " Box (area_norm {}): {} -> {}\n",
|
||||
area_norm, format_specs(this_specs), format_specs(other_specs));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RareItemSet::print_diff(FILE* stream, const RareItemSet& other) const {
|
||||
bool any_difference_found = false;
|
||||
for (const auto& episode : ALL_EPISODES_V4) {
|
||||
for (const auto& mode : ALL_GAME_MODES_V4) {
|
||||
for (const auto& difficulty : ALL_DIFFICULTIES_V234) {
|
||||
for (uint8_t section_id = 0; section_id < 10; section_id++) {
|
||||
const SpecCollection* this_coll = nullptr;
|
||||
const SpecCollection* other_coll = nullptr;
|
||||
try {
|
||||
this_coll = &this->get_collection(mode, episode, difficulty, section_id);
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
try {
|
||||
other_coll = &other.get_collection(mode, episode, difficulty, section_id);
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
|
||||
if (!this_coll && !other_coll) {
|
||||
continue;
|
||||
} else if (!this_coll) {
|
||||
any_difference_found = true;
|
||||
phosg::fwrite_fmt(stream, "> Collection present in other but not this: {} {} {} {}\n",
|
||||
name_for_episode(episode),
|
||||
name_for_mode(mode),
|
||||
name_for_difficulty(difficulty),
|
||||
name_for_section_id(section_id));
|
||||
} else if (!other_coll) {
|
||||
any_difference_found = true;
|
||||
phosg::fwrite_fmt(stream, "> Collection present in this but not other: {} {} {} {}\n",
|
||||
name_for_episode(episode),
|
||||
name_for_mode(mode),
|
||||
name_for_difficulty(difficulty),
|
||||
name_for_section_id(section_id));
|
||||
} else if (*this_coll != *other_coll) {
|
||||
any_difference_found = true;
|
||||
phosg::fwrite_fmt(stream, "> Collections do not match: {} {} {} {}\n",
|
||||
name_for_episode(episode),
|
||||
name_for_mode(mode),
|
||||
name_for_difficulty(difficulty),
|
||||
name_for_section_id(section_id));
|
||||
this_coll->print_diff(stream, *other_coll);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!any_difference_found) {
|
||||
phosg::fwrite_fmt(stream, "> These rare item sets are identical\n");
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<RareItemSet::ExpandedDrop> RareItemSet::get_enemy_specs(
|
||||
GameMode mode, Episode episode, Difficulty difficulty, uint8_t secid, uint8_t rt_index) const {
|
||||
GameMode mode, Episode episode, Difficulty difficulty, uint8_t secid, EnemyType enemy_type) const {
|
||||
try {
|
||||
return this->get_collection(mode, episode, difficulty, secid).rt_index_to_specs.at(rt_index);
|
||||
return this->get_collection(mode, episode, difficulty, secid).enemy_specs.at(enemy_type);
|
||||
} catch (const out_of_range&) {
|
||||
static const std::vector<ExpandedDrop> empty_vector;
|
||||
return empty_vector;
|
||||
@@ -947,7 +1023,7 @@ std::vector<RareItemSet::ExpandedDrop> RareItemSet::get_enemy_specs(
|
||||
std::vector<RareItemSet::ExpandedDrop> RareItemSet::get_box_specs(
|
||||
GameMode mode, Episode episode, Difficulty difficulty, uint8_t secid, uint8_t area_norm) const {
|
||||
try {
|
||||
return this->get_collection(mode, episode, difficulty, secid).box_area_norm_to_specs.at(area_norm);
|
||||
return this->get_collection(mode, episode, difficulty, secid).box_specs.at(area_norm);
|
||||
} catch (const out_of_range&) {
|
||||
static const std::vector<ExpandedDrop> empty_vector;
|
||||
return empty_vector;
|
||||
@@ -965,7 +1041,14 @@ bool RareItemSet::has_entries_for_game_config(GameMode mode, Episode episode, Di
|
||||
|
||||
const RareItemSet::SpecCollection& RareItemSet::get_collection(
|
||||
GameMode mode, Episode episode, Difficulty difficulty, uint8_t secid) const {
|
||||
return this->collections.at(this->key_for_params(mode, episode, difficulty, secid));
|
||||
try {
|
||||
return this->collections.at(this->key_for_params(mode, episode, difficulty, secid));
|
||||
} catch (const out_of_range&) {
|
||||
if (mode == GameMode::BATTLE || mode == GameMode::SOLO) {
|
||||
return this->collections.at(this->key_for_params(GameMode::NORMAL, episode, difficulty, secid));
|
||||
}
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t RareItemSet::key_for_params(GameMode mode, Episode episode, Difficulty difficulty, uint8_t secid) {
|
||||
|
||||
+16
-9
@@ -22,6 +22,9 @@ public:
|
||||
uint32_t probability = 0;
|
||||
ItemData data;
|
||||
|
||||
bool operator==(const ExpandedDrop& other) const = default;
|
||||
bool operator!=(const ExpandedDrop& other) const = default;
|
||||
|
||||
std::string str() const;
|
||||
std::string str(std::shared_ptr<const ItemNameIndex> name_index) const;
|
||||
};
|
||||
@@ -34,7 +37,7 @@ public:
|
||||
~RareItemSet() = default;
|
||||
|
||||
std::vector<ExpandedDrop> get_enemy_specs(
|
||||
GameMode mode, Episode episode, Difficulty difficulty, uint8_t secid, uint8_t rt_index) const;
|
||||
GameMode mode, Episode episode, Difficulty difficulty, uint8_t secid, EnemyType enemy_type) const;
|
||||
std::vector<ExpandedDrop> get_box_specs(
|
||||
GameMode mode, Episode episode, Difficulty difficulty, uint8_t secid, uint8_t area_norm) const;
|
||||
|
||||
@@ -60,11 +63,17 @@ public:
|
||||
uint8_t section_id,
|
||||
std::shared_ptr<const ItemNameIndex> name_index = nullptr) const;
|
||||
void print_all_collections(FILE* stream, std::shared_ptr<const ItemNameIndex> name_index = nullptr) const;
|
||||
void print_diff(FILE* stream, const RareItemSet& other) const;
|
||||
|
||||
protected:
|
||||
struct SpecCollection {
|
||||
std::vector<std::vector<ExpandedDrop>> rt_index_to_specs;
|
||||
std::vector<std::vector<ExpandedDrop>> box_area_norm_to_specs;
|
||||
std::unordered_map<EnemyType, std::vector<ExpandedDrop>> enemy_specs;
|
||||
std::vector<std::vector<ExpandedDrop>> box_specs; // Indexed by area_norm
|
||||
|
||||
bool operator==(const SpecCollection& other) const = default;
|
||||
bool operator!=(const SpecCollection& other) const = default;
|
||||
|
||||
void print_diff(FILE* stream, const SpecCollection& other) const;
|
||||
};
|
||||
|
||||
struct ParsedRELData {
|
||||
@@ -84,19 +93,17 @@ protected:
|
||||
/* 08 */ U32T<BE> box_areas_offset; // -> parray<uint8_t, 0x1E>
|
||||
/* 0C */ U32T<BE> box_rares_offset; // -> parray<PackedDrop, 0x1E>
|
||||
/* 10 */
|
||||
} __attribute__((packed));
|
||||
} __packed_ws_be__(OffsetsT, 0x10);
|
||||
using Offsets = OffsetsT<false>;
|
||||
using OffsetsBE = OffsetsT<true>;
|
||||
check_struct_size(Offsets, 0x10);
|
||||
check_struct_size(OffsetsBE, 0x10);
|
||||
|
||||
struct BoxRare {
|
||||
uint8_t area_norm_plus_1;
|
||||
ExpandedDrop drop;
|
||||
};
|
||||
|
||||
std::vector<ExpandedDrop> monster_rares;
|
||||
std::vector<BoxRare> box_rares;
|
||||
std::vector<ExpandedDrop> monster_rares; // Indexed by rt_index
|
||||
std::vector<BoxRare> box_rares; // Not indexed (area_norm + 1 is in the struct)
|
||||
|
||||
ParsedRELData() = default;
|
||||
ParsedRELData(phosg::StringReader r, bool big_endian, bool is_v1);
|
||||
@@ -108,7 +115,7 @@ protected:
|
||||
template <bool BE>
|
||||
std::string serialize_t(bool is_v1) const;
|
||||
|
||||
SpecCollection as_collection() const;
|
||||
SpecCollection as_collection(Episode episode) const;
|
||||
};
|
||||
|
||||
std::unordered_map<uint16_t, SpecCollection> collections;
|
||||
|
||||
+636
-73
File diff suppressed because it is too large
Load Diff
+441
-92
@@ -2,7 +2,9 @@
|
||||
|
||||
#include <math.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#include <fstream>
|
||||
#include <memory>
|
||||
#include <phosg/Random.hh>
|
||||
#include <phosg/Strings.hh>
|
||||
@@ -76,6 +78,184 @@ using SDF = SubcommandDefinition::Flag;
|
||||
|
||||
extern const vector<SubcommandDefinition> subcommand_definitions;
|
||||
|
||||
|
||||
|
||||
static string json_escape_for_hardcore_ledger(const string& text) {
|
||||
string ret;
|
||||
ret.reserve(text.size() + 16);
|
||||
for (char ch : text) {
|
||||
switch (ch) {
|
||||
case '\\':
|
||||
ret += "\\\\";
|
||||
break;
|
||||
case '"':
|
||||
ret += "\\\"";
|
||||
break;
|
||||
case '\n':
|
||||
ret += "\\n";
|
||||
break;
|
||||
case '\r':
|
||||
ret += "\\r";
|
||||
break;
|
||||
case '\t':
|
||||
ret += "\\t";
|
||||
break;
|
||||
default:
|
||||
ret += ch;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
static string hardcore_death_character_name_for_ledger(shared_ptr<Client> c) {
|
||||
try {
|
||||
auto p = c->character_file(false);
|
||||
if (p) {
|
||||
string name = p->disp.name.decode(c->language());
|
||||
if (!name.empty()) {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
} catch (const exception&) {
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
static string hardcore_death_ledger_filename(shared_ptr<Client> c) {
|
||||
string char_filename = c->character_filename();
|
||||
size_t slash_offset = char_filename.find_last_of('/');
|
||||
if (slash_offset == string::npos) {
|
||||
return "hardcore-deaths.jsonl";
|
||||
}
|
||||
return char_filename.substr(0, slash_offset + 1) + "hardcore-deaths.jsonl";
|
||||
}
|
||||
|
||||
|
||||
static string hardcore_zone_name(shared_ptr<Client> c) {
|
||||
// Basic Episode 1 floor names. Unknown Episode 2/4 floors will fall back safely for now.
|
||||
switch (c->floor) {
|
||||
case 0:
|
||||
return "Pioneer 2";
|
||||
case 1:
|
||||
return "Forest 1";
|
||||
case 2:
|
||||
return "Forest 2";
|
||||
case 3:
|
||||
return "Caves 1";
|
||||
case 4:
|
||||
return "Caves 2";
|
||||
case 5:
|
||||
return "Caves 3";
|
||||
case 6:
|
||||
return "Mines 1";
|
||||
case 7:
|
||||
return "Mines 2";
|
||||
case 8:
|
||||
return "Ruins 1";
|
||||
case 9:
|
||||
return "Ruins 2";
|
||||
case 10:
|
||||
return "Ruins 3";
|
||||
case 11:
|
||||
return "Dragon";
|
||||
case 12:
|
||||
return "De Rol Le";
|
||||
case 13:
|
||||
return "Vol Opt";
|
||||
case 14:
|
||||
return "Dark Falz";
|
||||
default:
|
||||
return std::format("unknown zone {}", c->floor);
|
||||
}
|
||||
}
|
||||
|
||||
static bool append_hardcore_death_ledger(shared_ptr<Client> c, const char* reason) {
|
||||
string filename = hardcore_death_ledger_filename(c);
|
||||
ofstream f(filename, ios::out | ios::app);
|
||||
if (!f.good()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t account_id = 0;
|
||||
string username;
|
||||
if (c->login) {
|
||||
if (c->login->account) {
|
||||
account_id = c->login->account->account_id;
|
||||
}
|
||||
if (c->login->bb_license) {
|
||||
username = c->login->bb_license->username;
|
||||
}
|
||||
}
|
||||
|
||||
string character_file = c->character_filename();
|
||||
string character_name = hardcore_death_character_name_for_ledger(c);
|
||||
string zone = hardcore_zone_name(c);
|
||||
|
||||
f << "{"
|
||||
<< "\"time_epoch\":" << static_cast<long long>(time(nullptr)) << ","
|
||||
<< "\"account_id\":" << account_id << ","
|
||||
<< "\"username\":\"" << json_escape_for_hardcore_ledger(username) << "\","
|
||||
<< "\"character_file\":\"" << json_escape_for_hardcore_ledger(character_file) << "\","
|
||||
<< "\"character_name\":\"" << json_escape_for_hardcore_ledger(character_name) << "\","
|
||||
<< "\"zone\":\"" << json_escape_for_hardcore_ledger(zone) << "\","
|
||||
<< "\"reason\":\"" << json_escape_for_hardcore_ledger(reason ? reason : "") << "\""
|
||||
<< "}\n";
|
||||
|
||||
return f.good();
|
||||
}
|
||||
|
||||
static string bb_hardcore_dead_filename_for_subcommands(shared_ptr<Client> c) {
|
||||
return c->character_filename() + ".hardcore-dead";
|
||||
}
|
||||
|
||||
static bool current_ship_is_hardcore_bb(shared_ptr<Client> c) {
|
||||
try {
|
||||
auto s = c->require_server_state();
|
||||
return s->enable_hardcore_mode && (c->version() == Version::BB_V4);
|
||||
} catch (const exception&) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static bool mark_bb_character_hardcore_dead_for_subcommands(shared_ptr<Client> c, const char* reason) {
|
||||
string filename = bb_hardcore_dead_filename_for_subcommands(c);
|
||||
|
||||
bool already_dead = false;
|
||||
{
|
||||
ifstream existing_f(filename);
|
||||
already_dead = existing_f.good();
|
||||
}
|
||||
|
||||
ofstream f(filename, ios::out | ios::trunc);
|
||||
if (!f.good()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t account_id = 0;
|
||||
if (c->login && c->login->account) {
|
||||
account_id = c->login->account->account_id;
|
||||
}
|
||||
|
||||
f << "status=hardcore-dead\n";
|
||||
f << "reason=" << reason << "\n";
|
||||
f << "account_id=" << account_id << "\n";
|
||||
f << "character_file=" << c->character_filename() << "\n";
|
||||
|
||||
bool ok = f.good();
|
||||
f.close();
|
||||
|
||||
if (ok && !already_dead) {
|
||||
if (append_hardcore_death_ledger(c, reason)) {
|
||||
c->log.info_f("Hardcore permadeath: appended death ledger entry for {}", c->character_filename());
|
||||
} else {
|
||||
c->log.warning_f("Hardcore permadeath: FAILED to append death ledger entry for {}", c->character_filename());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return ok;
|
||||
}
|
||||
|
||||
const SubcommandDefinition* def_for_subcommand(Version version, uint8_t subcommand) {
|
||||
static bool populated = false;
|
||||
static std::array<const SubcommandDefinition*, 0x100> nte_defs;
|
||||
@@ -399,7 +579,7 @@ asio::awaitable<void> forward_subcommand_with_entity_id_transcode_t(shared_ptr<C
|
||||
template <typename HeaderT>
|
||||
asio::awaitable<void> forward_subcommand_with_entity_targets_transcode_and_track_hits_t(
|
||||
shared_ptr<Client> c, SubcommandMessage& msg) {
|
||||
// I'm lazy and this should never happen for item commands (since all players need to stay in sync)
|
||||
// I'm lazy and this should never happen for private commands
|
||||
if (command_is_private(msg.command)) {
|
||||
throw runtime_error("entity subcommand sent via private command");
|
||||
}
|
||||
@@ -430,16 +610,12 @@ asio::awaitable<void> forward_subcommand_with_entity_targets_transcode_and_track
|
||||
if ((res.entity_id >= 0x1000) && (res.entity_id < 0x4000)) {
|
||||
auto ene_st = l->map_state->enemy_state_for_index(c->version(), res.entity_id - 0x1000);
|
||||
res.ene_st = ene_st;
|
||||
|
||||
// Track hits for all resolved enemies
|
||||
c->log.info_f("Claiming last hit on E-{:03X}", ene_st->e_id);
|
||||
ene_st->set_last_hit_by_client_id(c->lobby_client_id);
|
||||
l->log.info_f("E-{:03X} last hit claimed", ene_st->e_id);
|
||||
if (ene_st->alias_target_ene_st) {
|
||||
c->log.info_f("Claiming last hit on E-{:03X} (alias of E-{:03X})",
|
||||
ene_st->alias_target_ene_st->e_id, ene_st->e_id);
|
||||
ene_st->alias_target_ene_st->set_last_hit_by_client_id(c->lobby_client_id);
|
||||
l->log.info_f("Alias target E-{:03X} last hit claimed", ene_st->alias_target_ene_st->e_id);
|
||||
}
|
||||
|
||||
} else if ((res.entity_id >= 0x4000) && (res.entity_id < 0xFFFF)) {
|
||||
res.obj_st = l->map_state->object_state_for_index(c->version(), res.entity_id - 0x4000);
|
||||
}
|
||||
@@ -803,8 +979,8 @@ Parsed6x70Data::Parsed6x70Data(
|
||||
death_flags(cmd.death_flags),
|
||||
hold_state(cmd.hold_state),
|
||||
area(cmd.area),
|
||||
game_flags(cmd.game_flags),
|
||||
game_flags_is_v3(false),
|
||||
player_flags(cmd.player_flags),
|
||||
player_flags_is_v3(false),
|
||||
visual(cmd.visual),
|
||||
stats(cmd.stats),
|
||||
num_items(cmd.num_items),
|
||||
@@ -839,8 +1015,8 @@ Parsed6x70Data::Parsed6x70Data(
|
||||
death_flags(cmd.death_flags),
|
||||
hold_state(cmd.hold_state),
|
||||
area(cmd.area),
|
||||
game_flags(cmd.game_flags),
|
||||
game_flags_is_v3(false),
|
||||
player_flags(cmd.player_flags),
|
||||
player_flags_is_v3(false),
|
||||
visual(cmd.visual),
|
||||
stats(cmd.stats),
|
||||
num_items(cmd.num_items),
|
||||
@@ -872,7 +1048,7 @@ Parsed6x70Data::Parsed6x70Data(
|
||||
Version from_version,
|
||||
bool from_client_customization)
|
||||
: Parsed6x70Data(cmd.base, guild_card_number, from_version, from_client_customization) {
|
||||
this->game_flags_is_v3 = true;
|
||||
this->player_flags_is_v3 = true;
|
||||
this->stats = cmd.stats;
|
||||
this->num_items = cmd.num_items;
|
||||
this->items = cmd.items;
|
||||
@@ -888,7 +1064,7 @@ Parsed6x70Data::Parsed6x70Data(
|
||||
Version from_version,
|
||||
bool from_client_customization)
|
||||
: Parsed6x70Data(cmd.base, guild_card_number, from_version, from_client_customization) {
|
||||
this->game_flags_is_v3 = true;
|
||||
this->player_flags_is_v3 = true;
|
||||
this->stats = cmd.stats;
|
||||
this->num_items = cmd.num_items;
|
||||
this->items = cmd.items;
|
||||
@@ -904,7 +1080,7 @@ Parsed6x70Data::Parsed6x70Data(
|
||||
Version from_version,
|
||||
bool from_client_customization)
|
||||
: Parsed6x70Data(cmd.base, guild_card_number, from_version, from_client_customization) {
|
||||
this->game_flags_is_v3 = true;
|
||||
this->player_flags_is_v3 = true;
|
||||
this->stats = cmd.stats;
|
||||
this->num_items = cmd.num_items;
|
||||
this->items = cmd.items;
|
||||
@@ -924,7 +1100,7 @@ G_SyncPlayerDispAndInventory_DCNTE_6x70 Parsed6x70Data::as_dc_nte(shared_ptr<Ser
|
||||
ret.death_flags = this->death_flags;
|
||||
ret.hold_state = this->hold_state;
|
||||
ret.area = this->area;
|
||||
ret.game_flags = this->get_game_flags(false);
|
||||
ret.player_flags = this->get_player_flags(false);
|
||||
ret.visual = this->visual;
|
||||
ret.stats = this->stats;
|
||||
ret.num_items = this->num_items;
|
||||
@@ -956,7 +1132,7 @@ G_SyncPlayerDispAndInventory_DC112000_6x70 Parsed6x70Data::as_dc_112000(shared_p
|
||||
ret.death_flags = this->death_flags;
|
||||
ret.hold_state = this->hold_state;
|
||||
ret.area = this->area;
|
||||
ret.game_flags = this->get_game_flags(false);
|
||||
ret.player_flags = this->get_player_flags(false);
|
||||
ret.visual = this->visual;
|
||||
ret.stats = this->stats;
|
||||
ret.num_items = this->num_items;
|
||||
@@ -1112,7 +1288,7 @@ Parsed6x70Data::Parsed6x70Data(
|
||||
temporary_status_effect(base.temporary_status_effect),
|
||||
attack_status_effect(base.attack_status_effect),
|
||||
defense_status_effect(base.defense_status_effect),
|
||||
unused_status_effect(base.unused_status_effect),
|
||||
unknown_a1_status_effect(base.unknown_a1_status_effect),
|
||||
language(static_cast<Language>(base.language32.load())),
|
||||
player_tag(base.player_tag),
|
||||
guild_card_number(guild_card_number), // Ignore the client's GC#
|
||||
@@ -1122,8 +1298,8 @@ Parsed6x70Data::Parsed6x70Data(
|
||||
death_flags(base.death_flags),
|
||||
hold_state(base.hold_state),
|
||||
area(base.area),
|
||||
game_flags(base.game_flags),
|
||||
game_flags_is_v3(!is_v1_or_v2(from_version)),
|
||||
player_flags(base.player_flags),
|
||||
player_flags_is_v3(!is_v1_or_v2(from_version)),
|
||||
technique_levels_v1(base.technique_levels_v1),
|
||||
visual(base.visual) {}
|
||||
|
||||
@@ -1136,7 +1312,7 @@ G_6x70_Base_V1 Parsed6x70Data::base_v1(bool is_v3) const {
|
||||
ret.temporary_status_effect = this->temporary_status_effect;
|
||||
ret.attack_status_effect = this->attack_status_effect;
|
||||
ret.defense_status_effect = this->defense_status_effect;
|
||||
ret.unused_status_effect = this->unused_status_effect;
|
||||
ret.unknown_a1_status_effect = this->unknown_a1_status_effect;
|
||||
ret.language32 = static_cast<size_t>(this->language);
|
||||
ret.player_tag = this->player_tag;
|
||||
ret.guild_card_number = this->guild_card_number;
|
||||
@@ -1146,46 +1322,73 @@ G_6x70_Base_V1 Parsed6x70Data::base_v1(bool is_v3) const {
|
||||
ret.death_flags = this->death_flags;
|
||||
ret.hold_state = this->hold_state;
|
||||
ret.area = this->area;
|
||||
ret.game_flags = this->get_game_flags(is_v3);
|
||||
ret.player_flags = this->get_player_flags(is_v3);
|
||||
ret.technique_levels_v1 = this->technique_levels_v1;
|
||||
ret.visual = this->visual;
|
||||
return ret;
|
||||
}
|
||||
|
||||
uint32_t Parsed6x70Data::convert_game_flags(uint32_t game_flags, bool to_v3) {
|
||||
// The format of game_flags for players was changed significantly between v2 and v3, and not accounting for this
|
||||
// results in odd effects like other characters not appearing when joining a game. Unfortunately, some bits were
|
||||
// deleted on v3 and other bits were added, so it doesn't suffice to simply store the most complete format of this
|
||||
// field - we have to be able to convert between the two.
|
||||
|
||||
// Bits on v2: JIHCBAzy xwvutsrq ponmlkji hgfedcba
|
||||
// Bits on v3: JIHGFEDC BAzyxwvu srqponkj hgfedcba
|
||||
// The bits ilmt were removed in v3 and the bits to their left were shifted right. The bits DEFG were added in v3 and
|
||||
// do not exist on v2. Known meanings for these bits so far:
|
||||
// o = is dead
|
||||
// n = should play hit animation
|
||||
// y = is near enemy
|
||||
// H = is enemy?
|
||||
// I = is object? (some entities have both H and I set though)
|
||||
// J = is item
|
||||
uint32_t Parsed6x70Data::convert_player_flags(uint32_t player_flags, bool to_v3) {
|
||||
// The format of player_flags was changed significantly between v2 and v3, and not accounting for this results in odd
|
||||
// effects like other characters not appearing when joining a game. Unfortunately, some bits were deleted on v3 and
|
||||
// other bits were added, so it doesn't suffice to simply store the most complete format of this field - we have to
|
||||
// be able to convert between the two. What's known about these bits (? indicates meaning/behavior is unverified):
|
||||
// V1/V2 V3/V4
|
||||
// 00000001 00000001 = player hold is set (see notes on 6x2C and 6x2D in CommandFormats.hh)
|
||||
// 00000002 00000002 = player hold is set for the purpose of dropping an item
|
||||
// 00000004 00000004 = is on a different floor from the local player (or is loading into game)
|
||||
// 00000008 00000008 = unknown (TODO: appears to be entirely unused, at least in v2 and later)
|
||||
// 00000010 00000010 = player has sent any update command (position, attack, etc.) this frame
|
||||
// 00000020 00000020 = unknown (TODO: appears to be entirely unused, at least in v2 and later)
|
||||
// 00000040 00000040 = unknown (TODO: appears to be entirely unused, at least in v2 and later)
|
||||
// 00000080 00000080 = is in windup animation for technique or PB
|
||||
// 00000100 -------- = unknown (TODO: appears to be entirely unused, at least in v2)
|
||||
// 00000200 00000100 = chat, pause, or quick menu is open (suppresses action palette)
|
||||
// 00000400 00000200 = is in PB cutscene
|
||||
// 00000800 -------- = player is standing in water (steps create splash particles)
|
||||
// 00001000 -------- = player is standing in grass (steps create grass particles)
|
||||
// 00002000 00000400 = action palette is disabled by p_action_disable or one of the TObjQuestColA* objects
|
||||
// 00004000 00000800 = is about to return to Pioneer 2 after a death (TODO: also set by p_return_guild)
|
||||
// 00008000 00001000 = cannot use telepipe / Ryuker (e.g. boss warps set this flag when the player is nearby)
|
||||
// 00010000 00002000 = unknown (TODO: appears to be entirely unused, at least in v3)
|
||||
// 00020000 00004000 = is teleporting as a result of 6x24 (set only briefly after appearing at destination)
|
||||
// 00040000 00008000 = is dead NPC (set by e.g. npc_crptalk_id when regsA[4] == 1)
|
||||
// 00080000 -------- = unknown (TODO: appears to be entirely unused, at least in v2)
|
||||
// 00100000 00010000 = has permanent trap vision (e.g. is android)
|
||||
// 00200000 00020000 = equipped items are invisible / intangible (e.g. in Pioneer 2)
|
||||
// 00400000 00040000 = is loading / changing floors (set by 6x22 and at game join, cleared by 6x23)
|
||||
// 00800000 00080000 = player data is in the process of being exported to save file format
|
||||
// 01000000 00100000 = initial cutscene with the Principal has started in offline free-play single-mode
|
||||
// 02000000 00200000 = is visible (set shortly after warping into a floor; remains set until next warp)
|
||||
// 04000000 00400000 = position is valid (therefore player can be rendered)
|
||||
// 08000000 00800000 = player is invisible if not local, but items are still visible (TODO: what about on BB?)
|
||||
// 10000000 01000000 = if set, player does not drop weapon on death
|
||||
// -------- 02000000 = player's room_id_in_custom_area does not match player's room_id (used by TObjRoomId when
|
||||
// param6 == 0x00010000; TODO: figure out what this does)
|
||||
// -------- 04000000 = is sitting in lobby chair
|
||||
// -------- 08000000 = using male animations / poses in lobby chair
|
||||
// -------- 10000000 = using alternate lobby chair pose (X+B instead of X+A on GC, for example)
|
||||
// -------- 20000000 = appears to be unused (there is no codepath that sets this flag); if set, the player will
|
||||
// freeze when attempting any lobby animation
|
||||
// -------- 40000000 = unknown (TODO: used in offline multi-player Challenge mode; see tree from 3OE1:801BE594)
|
||||
|
||||
if (to_v3) {
|
||||
return (game_flags & 0xE00000FF) |
|
||||
((game_flags & 0x00000600) >> 1) |
|
||||
((game_flags & 0x0007E000) >> 3) |
|
||||
((game_flags & 0x1FF00000) >> 4);
|
||||
return (player_flags & 0x000000FF) |
|
||||
((player_flags & 0x00000600) >> 1) |
|
||||
((player_flags & 0x0007E000) >> 3) |
|
||||
((player_flags & 0x1FF00000) >> 4);
|
||||
} else {
|
||||
return (game_flags & 0xE00000FF) |
|
||||
((game_flags << 1) & 0x00000600) |
|
||||
((game_flags << 3) & 0x0007E000) |
|
||||
((game_flags << 4) & 0x1FF00000);
|
||||
return (player_flags & 0x000000FF) |
|
||||
((player_flags << 1) & 0x00000600) |
|
||||
((player_flags << 3) & 0x0007E000) |
|
||||
((player_flags << 4) & 0x1FF00000);
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t Parsed6x70Data::get_game_flags(bool is_v3) const {
|
||||
return (this->game_flags_is_v3 == is_v3)
|
||||
? this->game_flags
|
||||
: Parsed6x70Data::convert_game_flags(this->game_flags, is_v3);
|
||||
uint32_t Parsed6x70Data::get_player_flags(bool is_v3) const {
|
||||
return (this->player_flags_is_v3 == is_v3)
|
||||
? this->player_flags
|
||||
: Parsed6x70Data::convert_player_flags(this->player_flags, is_v3);
|
||||
}
|
||||
|
||||
static asio::awaitable<void> on_sync_joining_player_disp_and_inventory(
|
||||
@@ -1636,7 +1839,21 @@ static asio::awaitable<void> on_player_died(shared_ptr<Client> c, SubcommandMess
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
|
||||
bool hardcore_death = current_ship_is_hardcore_bb(c);
|
||||
if (hardcore_death) {
|
||||
if (mark_bb_character_hardcore_dead_for_subcommands(c, "player-death-subcommand")) {
|
||||
c->log.warning_f("Hardcore permadeath: marked BB character dead: {}", c->character_filename());
|
||||
} else {
|
||||
c->log.error_f("Hardcore permadeath: FAILED to mark BB character dead: {}", c->character_filename());
|
||||
}
|
||||
}
|
||||
|
||||
forward_subcommand(c, msg);
|
||||
|
||||
if (hardcore_death) {
|
||||
send_message_box(c, "DADDY FALZ WINS");
|
||||
c->channel->disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
static asio::awaitable<void> on_player_revivable(shared_ptr<Client> c, SubcommandMessage& msg) {
|
||||
@@ -1647,8 +1864,23 @@ static asio::awaitable<void> on_player_revivable(shared_ptr<Client> c, Subcomman
|
||||
co_return;
|
||||
}
|
||||
|
||||
bool hardcore_death = current_ship_is_hardcore_bb(c);
|
||||
if (hardcore_death) {
|
||||
if (mark_bb_character_hardcore_dead_for_subcommands(c, "player-revivable-subcommand")) {
|
||||
c->log.warning_f("Hardcore permadeath: marked BB character dead from revivable state: {}", c->character_filename());
|
||||
} else {
|
||||
c->log.error_f("Hardcore permadeath: FAILED to mark BB character dead from revivable state: {}", c->character_filename());
|
||||
}
|
||||
}
|
||||
|
||||
forward_subcommand(c, msg);
|
||||
|
||||
if (hardcore_death) {
|
||||
send_message_box(c, "DADDY FALZ WINS");
|
||||
c->channel->disconnect();
|
||||
co_return;
|
||||
}
|
||||
|
||||
// Revive if infinite HP is enabled
|
||||
bool player_cheats_enabled = l->check_flag(Lobby::Flag::CHEATS_ENABLED) ||
|
||||
(c->login->account->check_flag(Account::Flag::CHEAT_ANYWHERE));
|
||||
@@ -1671,6 +1903,17 @@ static asio::awaitable<void> on_player_revived(shared_ptr<Client> c, SubcommandM
|
||||
|
||||
auto l = c->require_lobby();
|
||||
if (l->is_game()) {
|
||||
if (current_ship_is_hardcore_bb(c)) {
|
||||
if (mark_bb_character_hardcore_dead_for_subcommands(c, "player-revived-subcommand")) {
|
||||
c->log.warning_f("Hardcore permadeath: blocked revive and marked BB character dead: {}", c->character_filename());
|
||||
} else {
|
||||
c->log.error_f("Hardcore permadeath: FAILED to mark BB character dead while blocking revive: {}", c->character_filename());
|
||||
}
|
||||
send_message_box(c, "DADDY FALZ WINS");
|
||||
c->channel->disconnect();
|
||||
co_return;
|
||||
}
|
||||
|
||||
forward_subcommand(c, msg);
|
||||
if ((l->check_flag(Lobby::Flag::CHEATS_ENABLED) || (c->login->account->check_flag(Account::Flag::CHEAT_ANYWHERE))) &&
|
||||
c->check_flag(Client::Flag::INFINITE_HP_ENABLED)) {
|
||||
@@ -2241,6 +2484,30 @@ static asio::awaitable<void> on_pick_up_item_generic(
|
||||
co_return;
|
||||
}
|
||||
|
||||
// TODO: Figure out what the actual max range is; 30 is an overestimate
|
||||
double dist2 = fi->pos.dist2(c->pos);
|
||||
if (dist2 > 900.0) {
|
||||
const bool allow_dcv2_falz_pickup_distance_recovery =
|
||||
is_request &&
|
||||
(c->version() == Version::DC_V2) &&
|
||||
(l->episode == Episode::EP1) &&
|
||||
(floor == 0x0E) &&
|
||||
(fi->from_obj == nullptr) &&
|
||||
(fi->from_ene == nullptr) &&
|
||||
fi->visible_to_client(c->lobby_client_id);
|
||||
|
||||
if (allow_dcv2_falz_pickup_distance_recovery) {
|
||||
l->log.warning_f(
|
||||
"DC V2 Falz floor item pickup distance recovery: Player {} picking up {:08X}; dist2={} item_floor={} player_floor={}",
|
||||
client_id, item_id, dist2, static_cast<uint64_t>(floor), static_cast<uint64_t>(c->floor));
|
||||
} else {
|
||||
l->log.warning_f("Player {} requests to pick up {:08X}, but it is too far away (dist2={})",
|
||||
client_id, item_id, dist2);
|
||||
l->add_item(floor, fi);
|
||||
co_return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
p->add_item(fi->data, *s->item_stack_limits(c->version()));
|
||||
} catch (const out_of_range&) {
|
||||
@@ -2810,7 +3077,7 @@ DropReconcileResult reconcile_drop_request_with_map(
|
||||
bool is_box = (cmd.rt_index == 0x30);
|
||||
|
||||
DropReconcileResult res;
|
||||
res.effective_rt_index = 0xFF;
|
||||
res.effective_enemy_type = EnemyType::UNKNOWN;
|
||||
res.should_drop = true;
|
||||
res.ignore_def = (cmd.ignore_def != 0);
|
||||
if (!map) {
|
||||
@@ -2856,22 +3123,22 @@ DropReconcileResult reconcile_drop_request_with_map(
|
||||
res.ref_ene_st = map->enemy_state_for_index(version, cmd.floor, cmd.entity_index);
|
||||
res.target_ene_st = res.ref_ene_st->alias_target_ene_st ? res.ref_ene_st->alias_target_ene_st : res.ref_ene_st;
|
||||
uint8_t area = map->floor_to_area.at(res.target_ene_st->super_ene->floor);
|
||||
EnemyType type = res.target_ene_st->type(version, area, difficulty, event);
|
||||
res.effective_enemy_type = res.target_ene_st->type(version, area, difficulty, event);
|
||||
c->log.info_f("Drop check for E-{:03X} (target E-{:03X}, type {})",
|
||||
res.ref_ene_st->e_id, res.target_ene_st->e_id, phosg::name_for_enum(type));
|
||||
res.effective_rt_index = type_definition_for_enemy(type).rt_index;
|
||||
res.ref_ene_st->e_id, res.target_ene_st->e_id, phosg::name_for_enum(res.effective_enemy_type));
|
||||
uint8_t expected_rt_index = type_definition_for_enemy(res.effective_enemy_type).rt_index;
|
||||
bool mismatched_rt_index = false;
|
||||
if (cmd.rt_index != res.effective_rt_index) {
|
||||
if (cmd.rt_index != expected_rt_index) {
|
||||
// Special cases: BULCLAW => BULK and DARK_GUNNER => DEATH_GUNNER
|
||||
if (cmd.rt_index == 0x27 && type == EnemyType::BULCLAW) {
|
||||
if ((cmd.rt_index == 0x27) && (res.effective_enemy_type == EnemyType::BULCLAW)) {
|
||||
c->log.info_f("E-{:03X} killed as BULK instead of BULCLAW", res.target_ene_st->e_id);
|
||||
res.effective_rt_index = 0x27;
|
||||
} else if (cmd.rt_index == 0x23 && type == EnemyType::DARK_GUNNER) {
|
||||
res.effective_enemy_type = EnemyType::BULK;
|
||||
} else if ((cmd.rt_index == 0x23) && (res.effective_enemy_type == EnemyType::DARK_GUNNER)) {
|
||||
c->log.info_f("E-{:03X} killed as DEATH_GUNNER instead of DARK_GUNNER", res.target_ene_st->e_id);
|
||||
res.effective_rt_index = 0x23;
|
||||
res.effective_enemy_type = EnemyType::DEATH_GUNNER;
|
||||
} else {
|
||||
c->log.warning_f("rt_index {:02X} from command does not match entity\'s expected index {:02X}",
|
||||
cmd.rt_index, res.effective_rt_index);
|
||||
cmd.rt_index, expected_rt_index);
|
||||
mismatched_rt_index = true;
|
||||
}
|
||||
}
|
||||
@@ -2881,9 +3148,10 @@ DropReconcileResult reconcile_drop_request_with_map(
|
||||
}
|
||||
if (c->check_flag(Client::Flag::DEBUG_ENABLED)) {
|
||||
std::string rt_index_str = mismatched_rt_index
|
||||
? std::format(" $C4{:02X}->{:02X}$C5", cmd.rt_index, res.effective_rt_index)
|
||||
: std::format(" {:02X}", res.effective_rt_index);
|
||||
send_text_message_fmt(c, "$C5E-{:03X}{} {}", res.target_ene_st->e_id, rt_index_str, phosg::name_for_enum(type));
|
||||
? std::format(" $C4{:02X}->{:02X}$C5", cmd.rt_index, expected_rt_index)
|
||||
: std::format(" {:02X}", expected_rt_index);
|
||||
send_text_message_fmt(c, "$C5E-{:03X}{} {}",
|
||||
res.target_ene_st->e_id, rt_index_str, phosg::name_for_enum(res.effective_enemy_type));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2947,12 +3215,13 @@ static asio::awaitable<void> on_entity_drop_item_request(shared_ptr<Client> c, S
|
||||
}
|
||||
|
||||
if (rec.should_drop) {
|
||||
auto generate_item = [&]() -> ItemCreator::DropResult {
|
||||
auto generate_item_for_client = [&](std::shared_ptr<Client> c) -> ItemCreator::DropResult {
|
||||
bool force_rare = c->check_flag(Client::Flag::ALL_RARES_ENABLED);
|
||||
if (rec.obj_st) {
|
||||
if (rec.ignore_def) {
|
||||
l->log.info_f("Creating item from box {:04X} => K-{:03X} (area {:02X})",
|
||||
cmd.entity_index, rec.obj_st->k_id, cmd.effective_area);
|
||||
return l->item_creator->on_box_item_drop(cmd.effective_area);
|
||||
return l->item_creator->on_box_item_drop(cmd.effective_area, force_rare);
|
||||
} else {
|
||||
l->log.info_f(
|
||||
"Creating item from box {:04X} => K-{:03X} (area {:02X}; specialized with {:g} {:08X} {:08X} {:08X})",
|
||||
@@ -2963,7 +3232,7 @@ static asio::awaitable<void> on_entity_drop_item_request(shared_ptr<Client> c, S
|
||||
} else if (rec.target_ene_st) {
|
||||
l->log.info_f("Creating item from enemy {:04X} => E-{:03X} (area {:02X})",
|
||||
cmd.entity_index, rec.target_ene_st->e_id, cmd.effective_area);
|
||||
return l->item_creator->on_monster_item_drop(rec.effective_rt_index, cmd.effective_area);
|
||||
return l->item_creator->on_monster_item_drop(rec.effective_enemy_type, cmd.effective_area, force_rare);
|
||||
} else {
|
||||
throw runtime_error("neither object nor enemy were present");
|
||||
}
|
||||
@@ -2985,7 +3254,7 @@ static asio::awaitable<void> on_entity_drop_item_request(shared_ptr<Client> c, S
|
||||
throw logic_error("unhandled simple drop mode");
|
||||
case ServerDropMode::SERVER_SHARED:
|
||||
case ServerDropMode::SERVER_DUPLICATE: {
|
||||
auto res = generate_item();
|
||||
auto res = generate_item_for_client(c);
|
||||
if (res.item.empty()) {
|
||||
l->log.info_f("No item was created");
|
||||
} else {
|
||||
@@ -3023,7 +3292,7 @@ static asio::awaitable<void> on_entity_drop_item_request(shared_ptr<Client> c, S
|
||||
case ServerDropMode::SERVER_PRIVATE: {
|
||||
for (const auto& lc : l->clients) {
|
||||
if (lc && (rec.obj_st || (lc->floor == cmd.floor))) {
|
||||
auto res = generate_item();
|
||||
auto res = generate_item_for_client(lc);
|
||||
if (res.item.empty()) {
|
||||
l->log.info_f("No item was created for {}", lc->channel->name);
|
||||
} else {
|
||||
@@ -3434,17 +3703,69 @@ static asio::awaitable<void> on_update_enemy_state(shared_ptr<Client> c, Subcomm
|
||||
}
|
||||
auto ene_st = l->map_state->enemy_state_for_index(c->version(), cmd.enemy_index);
|
||||
uint32_t src_flags = is_big_endian(c->version()) ? bswap32(cmd.game_flags) : cmd.game_flags.load();
|
||||
if (src_flags & 0xD0000000) { // Don't allow player, object, or item flags to be set
|
||||
throw std::runtime_error("incorrect entity type flags in 6x0A command");
|
||||
}
|
||||
if (l->difficulty == Difficulty::ULTIMATE) {
|
||||
src_flags = (src_flags & 0xFFFFFFC0) | (ene_st->game_flags & 0x0000003F);
|
||||
}
|
||||
ene_st->game_flags = src_flags;
|
||||
ene_st->total_damage = cmd.total_damage;
|
||||
l->log.info_f("E-{:03X} updated to damage={} game_flags={:08X}", ene_st->e_id, ene_st->total_damage, ene_st->game_flags);
|
||||
|
||||
bool should_track_hit = (src_flags & 0x00000200); // "Enemy was hit" flag (see 6x0A comments in CommandFormats.hh)
|
||||
uint16_t incoming_total_damage = cmd.total_damage.load();
|
||||
bool is_boss = type_definition_for_enemy(ene_st->super_ene->type).is_boss();
|
||||
bool blocked_damage_rollback = is_boss && (incoming_total_damage < ene_st->total_damage);
|
||||
|
||||
if (should_track_hit) {
|
||||
ene_st->set_last_hit_by_client_id(c->lobby_client_id);
|
||||
}
|
||||
if (blocked_damage_rollback) {
|
||||
uint32_t old_game_flags = ene_st->game_flags;
|
||||
ene_st->game_flags |= src_flags;
|
||||
l->log.warning_f("Blocked boss damage rollback and merged flags via 6x0A from C-{} on E-{:03X}: incoming_damage={} current_damage={} incoming_flags={:08X} old_flags={:08X} merged_flags={:08X}",
|
||||
c->lobby_client_id, ene_st->e_id, incoming_total_damage, ene_st->total_damage, src_flags, old_game_flags, ene_st->game_flags);
|
||||
} else {
|
||||
ene_st->game_flags = src_flags;
|
||||
ene_st->total_damage = incoming_total_damage;
|
||||
}
|
||||
|
||||
// Forward the server-authoritative damage/flags, not stale lower state from the sender.
|
||||
cmd.total_damage = ene_st->total_damage;
|
||||
cmd.game_flags = is_big_endian(c->version()) ? phosg::bswap32(ene_st->game_flags) : ene_st->game_flags;
|
||||
|
||||
l->log.info_f("E-{:03X} updated to damage={} game_flags={:08X}; last hit {}{}",
|
||||
ene_st->e_id,
|
||||
ene_st->total_damage,
|
||||
ene_st->game_flags,
|
||||
should_track_hit ? "claimed" : "not claimed",
|
||||
blocked_damage_rollback ? " (damage rollback blocked)" : "");
|
||||
if (ene_st->alias_target_ene_st) {
|
||||
ene_st->alias_target_ene_st->game_flags = src_flags;
|
||||
ene_st->alias_target_ene_st->total_damage = cmd.total_damage;
|
||||
l->log.info_f("Alias target E-{:03X} updated to damage={} game_flags={:08X}",
|
||||
ene_st->alias_target_ene_st->e_id, ene_st->alias_target_ene_st->total_damage, ene_st->alias_target_ene_st->game_flags);
|
||||
bool alias_is_boss = type_definition_for_enemy(ene_st->alias_target_ene_st->super_ene->type).is_boss();
|
||||
bool alias_blocked_damage_rollback = alias_is_boss && (incoming_total_damage < ene_st->alias_target_ene_st->total_damage);
|
||||
|
||||
if (should_track_hit) {
|
||||
ene_st->alias_target_ene_st->set_last_hit_by_client_id(c->lobby_client_id);
|
||||
}
|
||||
if (alias_blocked_damage_rollback) {
|
||||
uint32_t old_alias_game_flags = ene_st->alias_target_ene_st->game_flags;
|
||||
ene_st->alias_target_ene_st->game_flags |= src_flags;
|
||||
l->log.warning_f("Blocked boss damage rollback and merged flags via 6x0A from C-{} on alias target E-{:03X}: incoming_damage={} current_damage={} incoming_flags={:08X} old_flags={:08X} merged_flags={:08X}",
|
||||
c->lobby_client_id,
|
||||
ene_st->alias_target_ene_st->e_id,
|
||||
incoming_total_damage,
|
||||
ene_st->alias_target_ene_st->total_damage,
|
||||
src_flags,
|
||||
old_alias_game_flags,
|
||||
ene_st->alias_target_ene_st->game_flags);
|
||||
} else {
|
||||
ene_st->alias_target_ene_st->game_flags = src_flags;
|
||||
ene_st->alias_target_ene_st->total_damage = incoming_total_damage;
|
||||
}
|
||||
l->log.info_f("Alias target E-{:03X} updated to damage={} game_flags={:08X}; last hit {}{}",
|
||||
ene_st->alias_target_ene_st->e_id,
|
||||
ene_st->alias_target_ene_st->total_damage,
|
||||
ene_st->alias_target_ene_st->game_flags,
|
||||
should_track_hit ? "claimed" : "not claimed",
|
||||
alias_blocked_damage_rollback ? " (damage rollback blocked)" : "");
|
||||
}
|
||||
|
||||
// TODO: It'd be nice if this worked on bosses too, but it seems we have to use each boss' specific state-syncing
|
||||
@@ -3492,8 +3813,18 @@ static asio::awaitable<void> on_incr_enemy_damage(shared_ptr<Client> c, Subcomma
|
||||
cmd.total_damage_before_hit.load(),
|
||||
cmd.current_hp_before_hit.load(),
|
||||
cmd.max_hp.load());
|
||||
|
||||
if (cmd.hit_amount > 0) {
|
||||
c->log.info_f("Claiming last hit on E-{:03X}", ene_st->e_id);
|
||||
ene_st->set_last_hit_by_client_id(c->lobby_client_id);
|
||||
}
|
||||
ene_st->total_damage = std::min<uint32_t>(ene_st->total_damage + cmd.hit_amount, cmd.max_hp);
|
||||
if (ene_st->alias_target_ene_st) {
|
||||
if (cmd.hit_amount > 0) {
|
||||
c->log.info_f("Claiming last hit on E-{:03X} (alias of E-{:03X})",
|
||||
ene_st->alias_target_ene_st->e_id, ene_st->e_id);
|
||||
ene_st->alias_target_ene_st->set_last_hit_by_client_id(c->lobby_client_id);
|
||||
}
|
||||
ene_st->alias_target_ene_st->total_damage = std::min<uint32_t>(
|
||||
ene_st->alias_target_ene_st->total_damage + cmd.hit_amount, cmd.max_hp);
|
||||
}
|
||||
@@ -3501,13 +3832,13 @@ static asio::awaitable<void> on_incr_enemy_damage(shared_ptr<Client> c, Subcomma
|
||||
co_await forward_subcommand_with_entity_id_transcode_t<G_IncrementEnemyDamage_Extension_6xE4>(c, msg);
|
||||
}
|
||||
|
||||
static asio::awaitable<void> on_set_enemy_low_game_flags_ultimate(shared_ptr<Client> c, SubcommandMessage& msg) {
|
||||
static asio::awaitable<void> on_set_enemy_status_effect_flags_ultimate(shared_ptr<Client> c, SubcommandMessage& msg) {
|
||||
auto& cmd = msg.check_size_t<G_SetEnemyLowGameFlagsUltimate_6x9C>();
|
||||
|
||||
if (command_is_private(msg.command) ||
|
||||
(cmd.header.entity_id < 0x1000) ||
|
||||
(cmd.header.entity_id >= 0x4000) ||
|
||||
(cmd.low_game_flags & 0xFFFFFFC0) ||
|
||||
(cmd.status_effect_flags & 0xFFFFFFC0) ||
|
||||
(c->lobby_client_id > 3)) {
|
||||
co_return;
|
||||
}
|
||||
@@ -3517,12 +3848,12 @@ static asio::awaitable<void> on_set_enemy_low_game_flags_ultimate(shared_ptr<Cli
|
||||
}
|
||||
|
||||
auto ene_st = l->map_state->enemy_state_for_index(c->version(), cmd.header.entity_id - 0x1000);
|
||||
if (!(ene_st->game_flags & cmd.low_game_flags)) {
|
||||
ene_st->game_flags |= cmd.low_game_flags;
|
||||
if (!(ene_st->game_flags & cmd.status_effect_flags)) {
|
||||
ene_st->game_flags |= cmd.status_effect_flags;
|
||||
l->log.info_f("E-{:03X} updated to game_flags={:08X}", ene_st->e_id, ene_st->game_flags);
|
||||
}
|
||||
if (ene_st->alias_target_ene_st && !(ene_st->alias_target_ene_st->game_flags & cmd.low_game_flags)) {
|
||||
ene_st->alias_target_ene_st->game_flags |= cmd.low_game_flags;
|
||||
if (ene_st->alias_target_ene_st && !(ene_st->alias_target_ene_st->game_flags & cmd.status_effect_flags)) {
|
||||
ene_st->alias_target_ene_st->game_flags |= cmd.status_effect_flags;
|
||||
l->log.info_f("Alias E-{:03X} updated to game_flags={:08X}",
|
||||
ene_st->alias_target_ene_st->e_id, ene_st->alias_target_ene_st->game_flags);
|
||||
}
|
||||
@@ -3653,6 +3984,18 @@ static asio::awaitable<void> on_dragon_actions_6x12(shared_ptr<Client> c, Subcom
|
||||
throw runtime_error("DRAGON enemy is an alias");
|
||||
}
|
||||
|
||||
l->log.info_f("Dragon 6x12 from C-{} on E-{:03X}: phase={:04X} unknown_a3={:04X} target_client_id={:08X} pos=({:g}, {:g}) damage={} game_flags={:08X} set_flags={:04X}",
|
||||
c->lobby_client_id,
|
||||
ene_st->e_id,
|
||||
cmd.phase.load(),
|
||||
cmd.unknown_a3.load(),
|
||||
cmd.target_client_id.load(),
|
||||
cmd.x.load(),
|
||||
cmd.z.load(),
|
||||
ene_st->total_damage,
|
||||
ene_st->game_flags,
|
||||
ene_st->set_flags);
|
||||
|
||||
G_DragonBossActions_GC_6x12 sw_cmd = {{cmd.header.subcommand, cmd.header.size, cmd.header.entity_id.load()},
|
||||
cmd.phase.load(), cmd.unknown_a3.load(), cmd.target_client_id.load(), cmd.x.load(), cmd.z.load()};
|
||||
bool sender_is_be = is_big_endian(c->version());
|
||||
@@ -4061,8 +4404,11 @@ static asio::awaitable<void> on_enemy_exp_request_bb(shared_ptr<Client> c, Subco
|
||||
// If the requesting player never hit this enemy, they are probably cheating; ignore the command. Also, each player
|
||||
// sends a 6xC8 if they ever hit the enemy; we only react to the first 6xC8 for each enemy (and give all relevant
|
||||
// players EXP then, if they deserve it).
|
||||
if (!ene_st->ever_hit_by_client_id(c->lobby_client_id) ||
|
||||
(ene_st->server_flags & MapState::EnemyState::Flag::EXP_GIVEN)) {
|
||||
if (!ene_st->ever_hit_by_client_id(c->lobby_client_id)) {
|
||||
l->log.warning_f("The requesting player did not hit this enemy; ignoring request");
|
||||
co_return;
|
||||
}
|
||||
if (ene_st->server_flags & MapState::EnemyState::Flag::EXP_GIVEN) {
|
||||
l->log.info_f("EXP already given for this enemy; ignoring request");
|
||||
co_return;
|
||||
}
|
||||
@@ -4248,9 +4594,6 @@ asio::awaitable<void> on_transfer_item_via_mail_message_bb(shared_ptr<Client> c,
|
||||
if (!l->is_game()) {
|
||||
throw runtime_error("6xCB command sent in non-game lobby");
|
||||
}
|
||||
if (!l->check_flag(Lobby::Flag::QUEST_IN_PROGRESS) && !l->check_flag(Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS)) {
|
||||
throw runtime_error("6xCB command sent during free play");
|
||||
}
|
||||
if (cmd.header.client_id != c->lobby_client_id) {
|
||||
throw runtime_error("6xCB command sent by incorrect client");
|
||||
}
|
||||
@@ -4439,7 +4782,7 @@ static asio::awaitable<void> on_destroy_floor_item(shared_ptr<Client> c, Subcomm
|
||||
static asio::awaitable<void> on_identify_item_bb(shared_ptr<Client> c, SubcommandMessage& msg) {
|
||||
auto l = c->require_lobby();
|
||||
if (!l->is_game()) {
|
||||
throw runtime_error("6xDA command sent in non-game lobby");
|
||||
throw runtime_error("6xB8 command sent in non-game lobby");
|
||||
}
|
||||
|
||||
if (c->version() == Version::BB_V4) {
|
||||
@@ -5377,10 +5720,8 @@ static asio::awaitable<void> on_upgrade_weapon_attribute_bb(shared_ptr<Client> c
|
||||
if (payment_item.stack_size(*s->item_stack_limits(c->version())) < cmd.payment_count) {
|
||||
throw runtime_error("not enough payment items present");
|
||||
}
|
||||
p->remove_item(payment_item.id, cmd.payment_count, *s->item_stack_limits(c->version()));
|
||||
send_destroy_item_to_lobby(c, payment_item.id, cmd.payment_count);
|
||||
|
||||
uint8_t attribute_amount = 0;
|
||||
int8_t attribute_amount = 0;
|
||||
if (cmd.payment_type == 1 && cmd.payment_count == 1) {
|
||||
attribute_amount = 30;
|
||||
} else if (cmd.payment_type == 0 && cmd.payment_count == 4) {
|
||||
@@ -5392,7 +5733,7 @@ static asio::awaitable<void> on_upgrade_weapon_attribute_bb(shared_ptr<Client> c
|
||||
}
|
||||
|
||||
size_t attribute_index = 0;
|
||||
for (size_t z = 6; z <= (item.has_kill_count() ? 10 : 8); z += 2) {
|
||||
for (size_t z = 6; z <= (item.has_kill_count() ? 8 : 10); z += 2) {
|
||||
if ((item.data1[z] == 0) || (!(item.data1[z] & 0x80) && (item.data1[z] == cmd.attribute))) {
|
||||
attribute_index = z;
|
||||
break;
|
||||
@@ -5401,8 +5742,16 @@ static asio::awaitable<void> on_upgrade_weapon_attribute_bb(shared_ptr<Client> c
|
||||
if (attribute_index == 0) {
|
||||
throw runtime_error("no available attribute slots");
|
||||
}
|
||||
int8_t new_attr_value = static_cast<int8_t>(item.data1[attribute_index + 1]) + attribute_amount;
|
||||
if (new_attr_value > 100) {
|
||||
throw runtime_error("bonus value exceeds 100");
|
||||
}
|
||||
|
||||
p->remove_item(payment_item.id, cmd.payment_count, *s->item_stack_limits(c->version()));
|
||||
send_destroy_item_to_lobby(c, payment_item.id, cmd.payment_count);
|
||||
|
||||
item.data1[attribute_index] = cmd.attribute;
|
||||
item.data1[attribute_index + 1] += attribute_amount;
|
||||
item.data1[attribute_index + 1] += new_attr_value;
|
||||
|
||||
send_destroy_item_to_lobby(c, item.id, 1);
|
||||
send_create_inventory_item_to_lobby(c, c->lobby_client_id, item);
|
||||
@@ -5585,7 +5934,7 @@ const vector<SubcommandDefinition> subcommand_definitions{
|
||||
/* 6x99 */ {NONE, NONE, 0x99, on_forward_check_game},
|
||||
/* 6x9A */ {NONE, NONE, 0x9A, on_forward_check_game_client},
|
||||
/* 6x9B */ {NONE, NONE, 0x9B, on_forward_check_game},
|
||||
/* 6x9C */ {NONE, NONE, 0x9C, on_set_enemy_low_game_flags_ultimate},
|
||||
/* 6x9C */ {NONE, NONE, 0x9C, on_set_enemy_status_effect_flags_ultimate},
|
||||
/* 6x9D */ {NONE, NONE, 0x9D, on_forward_check_game},
|
||||
/* 6x9E */ {NONE, NONE, 0x9E, forward_subcommand_m},
|
||||
/* 6x9F */ {NONE, NONE, 0x9F, forward_subcommand_with_entity_id_transcode_t<G_GalGryphonBossActions_6x9F>},
|
||||
|
||||
@@ -23,7 +23,7 @@ struct DropReconcileResult {
|
||||
// for drop computation (which may be the result of following an alias from the ref ene_st)
|
||||
std::shared_ptr<MapState::EnemyState> ref_ene_st;
|
||||
std::shared_ptr<MapState::EnemyState> target_ene_st;
|
||||
uint8_t effective_rt_index;
|
||||
EnemyType effective_enemy_type;
|
||||
bool should_drop;
|
||||
bool ignore_def;
|
||||
};
|
||||
@@ -52,7 +52,7 @@ public:
|
||||
StatusEffectState temporary_status_effect;
|
||||
StatusEffectState attack_status_effect;
|
||||
StatusEffectState defense_status_effect;
|
||||
StatusEffectState unused_status_effect;
|
||||
StatusEffectState unknown_a1_status_effect; // De Rol Le uses this?
|
||||
Language language = Language::JAPANESE;
|
||||
uint32_t player_tag = 0;
|
||||
uint32_t guild_card_number = 0;
|
||||
@@ -62,8 +62,8 @@ public:
|
||||
uint32_t death_flags = 0;
|
||||
PlayerHoldState hold_state;
|
||||
uint32_t area = 0;
|
||||
uint32_t game_flags = 0;
|
||||
bool game_flags_is_v3 = false;
|
||||
uint32_t player_flags = 0;
|
||||
bool player_flags_is_v3 = false;
|
||||
parray<uint8_t, 0x14> technique_levels_v1 = 0xFF;
|
||||
PlayerVisualConfig visual;
|
||||
std::string name;
|
||||
@@ -121,8 +121,8 @@ protected:
|
||||
Parsed6x70Data(
|
||||
const G_6x70_Base_V1& base, uint32_t guild_card_number, Version from_version, bool from_client_customization);
|
||||
G_6x70_Base_V1 base_v1(bool is_v3) const;
|
||||
static uint32_t convert_game_flags(uint32_t game_flags, bool to_v3);
|
||||
uint32_t get_game_flags(bool is_v3) const;
|
||||
static uint32_t convert_player_flags(uint32_t player_flags, bool to_v3);
|
||||
uint32_t get_player_flags(bool is_v3) const;
|
||||
};
|
||||
|
||||
bool validate_6xBB(G_SyncCardTradeServerState_Ep3_6xBB& cmd);
|
||||
|
||||
@@ -365,6 +365,9 @@ ReplaySession::ReplaySession(shared_ptr<ServerState> state, FILE* input_log, boo
|
||||
if (line == "### use psov2 crypt") {
|
||||
this->state->use_psov2_rand_crypt = true;
|
||||
}
|
||||
if (line == "### use legacy item random behavior") {
|
||||
this->state->use_legacy_item_random_behavior = true;
|
||||
}
|
||||
if (line.starts_with("### cc ")) {
|
||||
// ### cc $<chat command>
|
||||
if (this->clients.size() != 1) {
|
||||
|
||||
@@ -324,17 +324,17 @@ void PSOBBGuildCardFile::delete_duplicates() {
|
||||
{
|
||||
unordered_set<uint32_t> seen;
|
||||
size_t read_index = 0, write_index = 0;
|
||||
for (read_index = 0; read_index < this->blocked.size(); read_index++) {
|
||||
const auto& read_blocked = this->blocked[read_index];
|
||||
if (seen.emplace(read_blocked.guild_card_number).second) {
|
||||
for (read_index = 0; read_index < this->blocked_senders.size(); read_index++) {
|
||||
const auto& read_blocked_senders = this->blocked_senders[read_index];
|
||||
if (seen.emplace(read_blocked_senders.guild_card_number).second) {
|
||||
if (write_index != read_index) {
|
||||
this->blocked[write_index] = read_blocked;
|
||||
this->blocked_senders[write_index] = read_blocked_senders;
|
||||
}
|
||||
write_index++;
|
||||
}
|
||||
}
|
||||
for (; write_index < this->blocked.size(); write_index++) {
|
||||
this->blocked[write_index].clear();
|
||||
for (; write_index < this->blocked_senders.size(); write_index++) {
|
||||
this->blocked_senders[write_index].clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -158,11 +158,9 @@ struct WordSelectMessageT {
|
||||
ret.unknown_a4 = this->unknown_a4;
|
||||
return ret;
|
||||
}
|
||||
} __attribute__((packed));
|
||||
} __packed_ws_be__(WordSelectMessageT, 0x1C);
|
||||
using WordSelectMessage = WordSelectMessageT<false>;
|
||||
using WordSelectMessageBE = WordSelectMessageT<true>;
|
||||
check_struct_size(WordSelectMessage, 0x1C);
|
||||
check_struct_size(WordSelectMessageBE, 0x1C);
|
||||
|
||||
template <bool BE, TextEncoding Encoding, size_t MaxChars>
|
||||
struct SaveFileChatShortcutEntryT {
|
||||
@@ -994,7 +992,7 @@ struct PSOBBGuildCardFile {
|
||||
} __packed_ws__(Entry, 0x1BC);
|
||||
|
||||
/* 0000 */ PSOBBMinimalSystemFile system_file;
|
||||
/* 0114 */ parray<GuildCardBB, 0x1C> blocked;
|
||||
/* 0114 */ parray<GuildCardBB, 0x1C> blocked_senders;
|
||||
/* 1DF4 */ parray<uint8_t, 0x180> unknown_a2;
|
||||
/* 1F74 */ parray<Entry, 0x69> entries;
|
||||
/* D590 */
|
||||
|
||||
+118
-60
@@ -1,4 +1,5 @@
|
||||
#include "SendCommands.hh"
|
||||
#include "BattleParamsIndex.hh"
|
||||
|
||||
#include <inttypes.h>
|
||||
#include <stdarg.h>
|
||||
@@ -690,42 +691,103 @@ void send_guild_card_chunk_bb(shared_ptr<Client> c, size_t chunk_index) {
|
||||
send_command(c, 0x02DC, 0x00000000, &cmd, sizeof(cmd) - sizeof(cmd.data) + data_size);
|
||||
}
|
||||
|
||||
static const vector<string> stream_file_entries = {
|
||||
"ItemMagEdit.prs",
|
||||
"ItemPMT.prs",
|
||||
"BattleParamEntry.dat",
|
||||
"BattleParamEntry_on.dat",
|
||||
"BattleParamEntry_lab.dat",
|
||||
"BattleParamEntry_lab_on.dat",
|
||||
"BattleParamEntry_ep4.dat",
|
||||
"BattleParamEntry_ep4_on.dat",
|
||||
"PlyLevelTbl.prs",
|
||||
};
|
||||
|
||||
|
||||
static bool is_battle_param_stream_file_for_blueballz(const string& filename) {
|
||||
return (filename == "BattleParamEntry.dat") ||
|
||||
(filename == "BattleParamEntry_on.dat") ||
|
||||
(filename == "BattleParamEntry_lab.dat") ||
|
||||
(filename == "BattleParamEntry_lab_on.dat") ||
|
||||
(filename == "BattleParamEntry_ep4.dat") ||
|
||||
(filename == "BattleParamEntry_ep4_on.dat");
|
||||
}
|
||||
|
||||
static string bb_stream_file_data_for_client(shared_ptr<Client> c) {
|
||||
auto s = c->require_server_state();
|
||||
|
||||
int64_t effective_blueballz_hp_scale_tier = (c->selected_blueballz_tier >= 0)
|
||||
? c->selected_blueballz_tier
|
||||
: s->blueballz_enemy_hp_scale_tier;
|
||||
|
||||
if (effective_blueballz_hp_scale_tier < 0) {
|
||||
return s->bb_stream_file->data;
|
||||
}
|
||||
|
||||
effective_blueballz_hp_scale_tier = std::min<int64_t>(
|
||||
s->blueballz_max_tier,
|
||||
effective_blueballz_hp_scale_tier);
|
||||
|
||||
string scaled_data = s->bb_stream_file->data;
|
||||
double mult = 1.0 + (static_cast<double>(effective_blueballz_hp_scale_tier) * 0.25);
|
||||
size_t ultimate_index = static_cast<size_t>(Difficulty::ULTIMATE);
|
||||
|
||||
auto scale_u16 = [mult](uint32_t v) -> uint16_t {
|
||||
if (v == 0) {
|
||||
return 0;
|
||||
}
|
||||
uint32_t scaled = static_cast<uint32_t>(static_cast<double>(v) * mult);
|
||||
if (scaled < 1) {
|
||||
scaled = 1;
|
||||
}
|
||||
if (scaled > 0xFFFF) {
|
||||
scaled = 0xFFFF;
|
||||
}
|
||||
return static_cast<uint16_t>(scaled);
|
||||
};
|
||||
|
||||
for (const auto& sf_entry : s->bb_stream_file->entries) {
|
||||
if (!is_battle_param_stream_file_for_blueballz(sf_entry.filename)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((sf_entry.offset > scaled_data.size()) ||
|
||||
(sf_entry.size > (scaled_data.size() - sf_entry.offset))) {
|
||||
c->log.warning_f("Blueballz enemy HP scaling skipped for {}; invalid stream-file range",
|
||||
sf_entry.filename);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sf_entry.size < sizeof(BattleParamsIndex::Table)) {
|
||||
c->log.warning_f("Blueballz enemy HP scaling skipped for {}; file is too small",
|
||||
sf_entry.filename);
|
||||
continue;
|
||||
}
|
||||
|
||||
auto* table = reinterpret_cast<BattleParamsIndex::Table*>(
|
||||
scaled_data.data() + sf_entry.offset);
|
||||
|
||||
for (size_t z = 0; z < 0x60; z++) {
|
||||
auto& stats = table->stats[ultimate_index][z];
|
||||
stats.char_stats.hp = scale_u16(stats.char_stats.hp);
|
||||
}
|
||||
|
||||
c->log.info_f("Blueballz enemy HP scaling: serving {} with tier {} ({:g}x Ultimate HP)",
|
||||
sf_entry.filename, effective_blueballz_hp_scale_tier, mult);
|
||||
}
|
||||
|
||||
return scaled_data;
|
||||
}
|
||||
|
||||
|
||||
void send_stream_file_index_bb(shared_ptr<Client> c) {
|
||||
auto s = c->require_server_state();
|
||||
|
||||
c->log.info_f("PSO Peeps BBZ stream debug: send_stream_file_index_bb called");
|
||||
|
||||
vector<S_StreamFileIndexEntry_BB_01EB> entries;
|
||||
size_t offset = 0;
|
||||
for (const string& filename : stream_file_entries) {
|
||||
string key = "system/blueburst/" + filename;
|
||||
auto cache_res = s->bb_stream_files_cache->get_or_load(key);
|
||||
auto& e = entries.emplace_back();
|
||||
e.size = cache_res.file->data->size();
|
||||
// Computing the checksum can be slow, so we cache it along with the file data. If the cache result was just
|
||||
// populated, then it may be different, so we always recompute the checksum in that case.
|
||||
if (cache_res.generate_called) {
|
||||
e.checksum = crc32(cache_res.file->data->data(), e.size);
|
||||
s->bb_stream_files_cache->replace_obj<uint32_t>(key + ".crc32", e.checksum);
|
||||
} else {
|
||||
auto compute_checksum = [&](const string&) -> uint32_t {
|
||||
return crc32(cache_res.file->data->data(), e.size);
|
||||
};
|
||||
e.checksum = s->bb_stream_files_cache->get_obj<uint32_t>(key + ".crc32", compute_checksum).obj;
|
||||
string contents = bb_stream_file_data_for_client(c);
|
||||
for (const auto& sf_entry : s->bb_stream_file->entries) {
|
||||
if ((sf_entry.offset > contents.size()) ||
|
||||
(sf_entry.size > (contents.size() - sf_entry.offset))) {
|
||||
throw runtime_error("invalid BB stream file entry range");
|
||||
}
|
||||
e.offset = offset;
|
||||
e.filename.encode(filename);
|
||||
offset += e.size;
|
||||
|
||||
auto& e = entries.emplace_back();
|
||||
e.size = sf_entry.size;
|
||||
e.checksum = crc32(contents.data() + sf_entry.offset, e.size);
|
||||
e.offset = sf_entry.offset;
|
||||
e.filename.encode(sf_entry.filename);
|
||||
|
||||
}
|
||||
send_command_vt(c, 0x01EB, entries.size(), entries);
|
||||
}
|
||||
@@ -733,30 +795,17 @@ void send_stream_file_index_bb(shared_ptr<Client> c) {
|
||||
void send_stream_file_chunk_bb(shared_ptr<Client> c, uint32_t chunk_index) {
|
||||
auto s = c->require_server_state();
|
||||
|
||||
auto cache_result = s->bb_stream_files_cache->get(
|
||||
"<BB stream file>", [&](const string&) -> string {
|
||||
size_t bytes = 0;
|
||||
for (const auto& name : stream_file_entries) {
|
||||
bytes += s->bb_stream_files_cache->get_or_load("system/blueburst/" + name).file->data->size();
|
||||
}
|
||||
|
||||
string ret;
|
||||
ret.reserve(bytes);
|
||||
for (const auto& name : stream_file_entries) {
|
||||
ret += *s->bb_stream_files_cache->get_or_load("system/blueburst/" + name).file->data;
|
||||
}
|
||||
return ret;
|
||||
});
|
||||
const auto& contents = cache_result.file->data;
|
||||
string contents = bb_stream_file_data_for_client(c);
|
||||
|
||||
S_StreamFileChunk_BB_02EB chunk_cmd;
|
||||
chunk_cmd.chunk_index = chunk_index;
|
||||
size_t offset = sizeof(chunk_cmd.data) * chunk_index;
|
||||
if (offset > contents->size()) {
|
||||
if (offset > contents.size()) {
|
||||
throw runtime_error("client requested chunk beyond end of stream file");
|
||||
}
|
||||
size_t bytes = min<size_t>(contents->size() - offset, sizeof(chunk_cmd.data));
|
||||
chunk_cmd.data.assign_range(reinterpret_cast<const uint8_t*>(contents->data() + offset), bytes, 0);
|
||||
size_t bytes = min<size_t>(contents.size() - offset, sizeof(chunk_cmd.data));
|
||||
chunk_cmd.data.assign_range(reinterpret_cast<const uint8_t*>(contents.data() + offset), bytes, 0);
|
||||
|
||||
|
||||
size_t cmd_size = offsetof(S_StreamFileChunk_BB_02EB, data) + bytes;
|
||||
cmd_size = (cmd_size + 3) & ~3;
|
||||
@@ -1139,16 +1188,25 @@ void send_simple_mail(shared_ptr<ServerState> s, uint32_t from_guild_card_number
|
||||
template <TextEncoding NameEncoding, TextEncoding MessageEncoding>
|
||||
void send_info_board_t(shared_ptr<Client> c) {
|
||||
vector<S_InfoBoardEntryT_D8<NameEncoding, MessageEncoding>> entries;
|
||||
auto l = c->require_lobby();
|
||||
for (const auto& other_c : l->clients) {
|
||||
if (!other_c.get()) {
|
||||
continue;
|
||||
|
||||
auto add_clients_from_lobby = [&](std::shared_ptr<Lobby> l) -> void {
|
||||
for (const auto& lc : l->clients) {
|
||||
if (lc) {
|
||||
auto lp = lc->character_file(true, false);
|
||||
auto& e = entries.emplace_back();
|
||||
e.name.encode(lp->disp.name.decode(lp->inventory.language), c->language());
|
||||
e.message.encode(add_color(lp->info_board.decode(lp->inventory.language)), c->language());
|
||||
}
|
||||
}
|
||||
auto other_p = other_c->character_file(true, false);
|
||||
auto& e = entries.emplace_back();
|
||||
e.name.encode(other_p->disp.name.decode(other_p->inventory.language), c->language());
|
||||
e.message.encode(add_color(other_p->info_board.decode(other_p->inventory.language)), c->language());
|
||||
};
|
||||
|
||||
auto l = c->require_lobby();
|
||||
auto watched_l = l->watched_lobby.lock();
|
||||
if (watched_l) {
|
||||
add_clients_from_lobby(watched_l);
|
||||
}
|
||||
add_clients_from_lobby(l);
|
||||
|
||||
send_command_vt(c, 0xD8, entries.size(), entries);
|
||||
}
|
||||
|
||||
@@ -2654,16 +2712,16 @@ asio::awaitable<void> send_change_player_hp(
|
||||
}
|
||||
|
||||
asio::awaitable<void> send_remove_negative_conditions(shared_ptr<Client> c) {
|
||||
G_AddStatusEffect_6x0C cmd;
|
||||
cmd.header = {0x0C, sizeof(G_AddStatusEffect_6x0C) >> 2, c->lobby_client_id};
|
||||
G_AddOrRemoveStatusEffect_6x0C_6x0D cmd;
|
||||
cmd.header = {0x0C, sizeof(G_AddOrRemoveStatusEffect_6x0C_6x0D) >> 2, c->lobby_client_id};
|
||||
cmd.effect_type = 7; // Healing ring
|
||||
cmd.amount = 0;
|
||||
co_await send_protected_command(c, &cmd, sizeof(cmd), true);
|
||||
}
|
||||
|
||||
void send_remove_negative_conditions(std::shared_ptr<Channel> ch, uint16_t client_id) {
|
||||
G_AddStatusEffect_6x0C cmd;
|
||||
cmd.header = {0x0C, sizeof(G_AddStatusEffect_6x0C) >> 2, client_id};
|
||||
G_AddOrRemoveStatusEffect_6x0C_6x0D cmd;
|
||||
cmd.header = {0x0C, sizeof(G_AddOrRemoveStatusEffect_6x0C_6x0D) >> 2, client_id};
|
||||
cmd.effect_type = 7; // Healing ring
|
||||
cmd.amount = 0;
|
||||
ch->send(0x60, 0x00, &cmd, sizeof(cmd));
|
||||
|
||||
+115
-38
@@ -73,39 +73,54 @@ ServerState::QuestF960Result::QuestF960Result(
|
||||
}
|
||||
}
|
||||
|
||||
ServerState::ServerState(const string& config_filename)
|
||||
ServerState::ServerState(const string& config_filename, bool is_replay)
|
||||
: creation_time(phosg::now()),
|
||||
io_context(make_shared<asio::io_context>(1)),
|
||||
config_filename(config_filename),
|
||||
is_replay(is_replay),
|
||||
thread_pool(make_unique<asio::thread_pool>()),
|
||||
bb_stream_files_cache(new FileContentsCache(3600000000ULL)),
|
||||
bb_system_cache(new FileContentsCache(3600000000ULL)),
|
||||
gba_files_cache(new FileContentsCache(3600000000ULL)) {}
|
||||
|
||||
void ServerState::add_client_to_available_lobby(shared_ptr<Client> c) {
|
||||
void ServerState::add_client_to_available_lobby(shared_ptr<Client> c, bool allow_games) {
|
||||
shared_ptr<Lobby> added_to_lobby;
|
||||
|
||||
if (c->preferred_lobby_id >= 0) {
|
||||
try {
|
||||
auto l = this->find_lobby(c->preferred_lobby_id);
|
||||
if (l && !l->is_game() && l->check_flag(Lobby::Flag::PUBLIC) && l->version_is_allowed(c->version())) {
|
||||
l->add_client(c);
|
||||
added_to_lobby = l;
|
||||
}
|
||||
} catch (const out_of_range&) {
|
||||
auto try_join_lobby = [&](uint32_t lobby_id) -> std::shared_ptr<Lobby> {
|
||||
auto l = this->find_lobby(lobby_id);
|
||||
if (!l) {
|
||||
c->log.info_f("Cannot join lobby {:08X}: lobby does not exist", lobby_id);
|
||||
return nullptr;
|
||||
}
|
||||
if (!allow_games && l->is_game()) {
|
||||
c->log.info_f("Cannot join lobby {:08X}: lobby is a game", lobby_id);
|
||||
return nullptr;
|
||||
}
|
||||
static const std::string password = "";
|
||||
auto join_error = l->join_error_for_client(c, &password);
|
||||
if (join_error == Lobby::JoinError::ALLOWED) {
|
||||
try {
|
||||
l->add_client(c);
|
||||
c->log.info_f("Joined lobby {:08X}", lobby_id);
|
||||
return l;
|
||||
} catch (const out_of_range& e) {
|
||||
c->log.info_f("Cannot join lobby {:08X}: {}", lobby_id, e.what());
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
c->log.info_f("Cannot join lobby {:08X}: {}", lobby_id, phosg::name_for_enum(join_error));
|
||||
return nullptr;
|
||||
};
|
||||
|
||||
if (c->preferred_lobby_id >= 0) {
|
||||
added_to_lobby = try_join_lobby(c->preferred_lobby_id);
|
||||
c->preferred_lobby_id = -1;
|
||||
}
|
||||
|
||||
if (!added_to_lobby.get()) {
|
||||
if (!added_to_lobby) {
|
||||
for (const auto& lobby_id : this->public_lobby_search_order(c)) {
|
||||
try {
|
||||
auto l = this->find_lobby(lobby_id);
|
||||
if (l && !l->is_game() && l->check_flag(Lobby::Flag::PUBLIC) && l->version_is_allowed(c->version())) {
|
||||
l->add_client(c);
|
||||
added_to_lobby = l;
|
||||
break;
|
||||
}
|
||||
} catch (const out_of_range&) {
|
||||
added_to_lobby = try_join_lobby(lobby_id);
|
||||
if (added_to_lobby) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -259,7 +274,7 @@ shared_ptr<Client> ServerState::find_client(const string* identifier, uint64_t a
|
||||
|
||||
for (auto& other_l : this->all_lobbies()) {
|
||||
if (l == other_l) {
|
||||
continue; // don't bother looking again
|
||||
continue; // Don't bother looking again
|
||||
}
|
||||
try {
|
||||
return other_l->find_client(identifier, account_id);
|
||||
@@ -319,7 +334,7 @@ uint16_t ServerState::game_server_port_for_version(Version v) const {
|
||||
case Version::XB_V3:
|
||||
return this->name_to_port_config.at("xb")->port;
|
||||
case Version::BB_V4:
|
||||
return this->name_to_port_config.at("xb")->port;
|
||||
return this->name_to_port_config.at("bb-data1")->port;
|
||||
default:
|
||||
throw runtime_error("unknown version");
|
||||
}
|
||||
@@ -460,8 +475,10 @@ shared_ptr<const ItemParameterTable> ServerState::item_parameter_table_for_encod
|
||||
}
|
||||
|
||||
shared_ptr<const MagEvolutionTable> ServerState::mag_evolution_table(Version version) const {
|
||||
if (is_v1_or_v2(version)) {
|
||||
return this->mag_evolution_table_v1_v2;
|
||||
if (is_v1(version)) {
|
||||
return this->mag_evolution_table_v1;
|
||||
} else if (is_v2(version)) {
|
||||
return this->mag_evolution_table_v2;
|
||||
} else if (!is_v4(version)) {
|
||||
return this->mag_evolution_table_v3;
|
||||
} else {
|
||||
@@ -505,7 +522,12 @@ ItemData ServerState::parse_item_description(Version version, const string& desc
|
||||
|
||||
shared_ptr<const CommonItemSet> ServerState::common_item_set(Version logic_version, shared_ptr<const Quest> q) const {
|
||||
if (q && !q->meta.common_item_set_name.empty()) {
|
||||
return this->common_item_sets.at(q->meta.common_item_set_name);
|
||||
try {
|
||||
return this->common_item_sets.at(q->meta.common_item_set_name);
|
||||
} catch (const std::out_of_range&) {
|
||||
throw runtime_error(std::format("common item set {} for quest {} does not exist",
|
||||
q->meta.common_item_set_name, q->meta.name));
|
||||
}
|
||||
} else if (is_v1_or_v2(logic_version) && (logic_version != Version::GC_NTE)) {
|
||||
// TODO: We should probably have a v1 common item set at some point too
|
||||
return this->common_item_sets.at("common-table-v1-v2");
|
||||
@@ -519,7 +541,12 @@ shared_ptr<const CommonItemSet> ServerState::common_item_set(Version logic_versi
|
||||
|
||||
shared_ptr<const RareItemSet> ServerState::rare_item_set(Version logic_version, shared_ptr<const Quest> q) const {
|
||||
if (q && !q->meta.rare_item_set_name.empty()) {
|
||||
return this->rare_item_sets.at(q->meta.rare_item_set_name);
|
||||
try {
|
||||
return this->rare_item_sets.at(q->meta.rare_item_set_name);
|
||||
} catch (const std::out_of_range&) {
|
||||
throw runtime_error(std::format("rare item set {} for quest {} does not exist",
|
||||
q->meta.rare_item_set_name, q->meta.name));
|
||||
}
|
||||
} else if (is_v1(logic_version)) {
|
||||
return this->rare_item_sets.at("rare-table-v1");
|
||||
} else if (is_v2(logic_version) && (logic_version != Version::GC_NTE)) {
|
||||
@@ -904,6 +931,22 @@ void ServerState::load_config_early() {
|
||||
this->cheat_mode_behavior = parse_behavior_switch("CheatModeBehavior", BehaviorSwitch::OFF_BY_DEFAULT);
|
||||
this->default_switch_assist_enabled = this->config_json->get_bool("EnableSwitchAssistByDefault", false);
|
||||
this->use_game_creator_section_id = this->config_json->get_bool("UseGameCreatorSectionID", false);
|
||||
this->enable_bb_ship_selection_menu = this->config_json->get_bool("EnableBBShipSelectionMenu", false);
|
||||
this->enable_blueballz = this->config_json->get_bool("EnableBlueballz", false);
|
||||
this->enable_hardcore_mode = this->config_json->get_bool("EnableHardcoreMode", false);
|
||||
this->blueballz_max_tier = std::min<int64_t>(10, std::max<int64_t>(0, this->config_json->get_int("BlueballzMaxTier", 10)));
|
||||
this->blueballz_unlocked_tier_v2 = std::min<int64_t>(
|
||||
this->blueballz_max_tier,
|
||||
std::max<int64_t>(-1, this->config_json->get_int("BlueballzUnlockedTierV2", -1)));
|
||||
this->blueballz_unlocked_tier_v3 = std::min<int64_t>(
|
||||
this->blueballz_max_tier,
|
||||
std::max<int64_t>(-1, this->config_json->get_int("BlueballzUnlockedTierV3", -1)));
|
||||
this->blueballz_unlocked_tier_v4 = std::min<int64_t>(
|
||||
this->blueballz_max_tier,
|
||||
std::max<int64_t>(-1, this->config_json->get_int("BlueballzUnlockedTierV4", -1)));
|
||||
this->blueballz_enemy_hp_scale_tier = std::min<int64_t>(
|
||||
this->blueballz_max_tier,
|
||||
std::max<int64_t>(-1, this->config_json->get_int("BlueballzEnemyHPScaleTier", -1)));
|
||||
this->rare_notifs_enabled_for_client_drops = this->config_json->get_bool("RareNotificationsEnabledForClientDrops", false);
|
||||
this->default_rare_notifs_enabled_v1_v2 = this->config_json->get_bool("RareNotificationsEnabledByDefault", false);
|
||||
this->default_rare_notifs_enabled_v3_v4 = this->default_rare_notifs_enabled_v1_v2;
|
||||
@@ -1803,8 +1846,6 @@ vector<shared_ptr<const SuperMap>> ServerState::supermaps_for_variations(
|
||||
}
|
||||
|
||||
void ServerState::clear_file_caches() {
|
||||
config_log.info_f("Clearing BB stream file cache");
|
||||
this->bb_stream_files_cache.reset(new FileContentsCache(3600000000ULL));
|
||||
config_log.info_f("Clearing BB system cache");
|
||||
this->bb_system_cache.reset(new FileContentsCache(3600000000ULL));
|
||||
config_log.info_f("Clearing GBA file cache");
|
||||
@@ -2120,29 +2161,31 @@ void ServerState::load_item_definitions() {
|
||||
config_log.info_f("Loading item definition tables");
|
||||
for (size_t v_s = NUM_PATCH_VERSIONS; v_s < NUM_VERSIONS; v_s++) {
|
||||
Version v = static_cast<Version>(v_s);
|
||||
string path = std::format("system/item-tables/ItemPMT-{}.prs", file_path_token_for_version(v));
|
||||
string path = std::format("system/item-tables/item-parameter-table-{}.json", file_path_token_for_version(v));
|
||||
config_log.debug_f("Loading item definition table {}", path);
|
||||
auto data = make_shared<string>(prs_decompress(phosg::load_file(path)));
|
||||
new_item_parameter_tables[v_s] = make_shared<ItemParameterTable>(data, v);
|
||||
new_item_parameter_tables[v_s] = ItemParameterTable::from_json(phosg::JSON::parse(phosg::load_file(path)));
|
||||
}
|
||||
|
||||
auto json = phosg::JSON::parse(phosg::load_file("system/item-tables/translation-table.json"));
|
||||
auto new_item_translation_table = make_shared<ItemTranslationTable>(json, new_item_parameter_tables);
|
||||
|
||||
// TODO: We should probably load the tables for other versions too.
|
||||
config_log.info_f("Loading v1/v2 mag evolution table");
|
||||
auto mag_data_v1_v2 = make_shared<string>(prs_decompress(phosg::load_file("system/item-tables/ItemMagEdit-dc-v2.prs")));
|
||||
auto new_table_v1_v2 = make_shared<MagEvolutionTable>(mag_data_v1_v2, 0x3A);
|
||||
config_log.info_f("Loading v1 mag evolution table");
|
||||
auto mag_data_v1 = make_shared<string>(prs_decompress(phosg::load_file("system/item-tables/ItemMagEdit-dc-v1.prs")));
|
||||
auto new_table_v1 = MagEvolutionTable::create(mag_data_v1, Version::DC_V1);
|
||||
config_log.info_f("Loading v2 mag evolution table");
|
||||
auto mag_data_v2 = make_shared<string>(prs_decompress(phosg::load_file("system/item-tables/ItemMagEdit-dc-v2.prs")));
|
||||
auto new_table_v2 = MagEvolutionTable::create(mag_data_v2, Version::DC_V2);
|
||||
config_log.info_f("Loading v3 mag evolution table");
|
||||
auto mag_data_v3 = make_shared<string>(prs_decompress(phosg::load_file("system/item-tables/ItemMagEdit-xb-v3.prs")));
|
||||
auto new_table_v3 = make_shared<MagEvolutionTable>(mag_data_v3, 0x43);
|
||||
auto new_table_v3 = MagEvolutionTable::create(mag_data_v3, Version::XB_V3);
|
||||
config_log.info_f("Loading v4 mag evolution table");
|
||||
auto mag_data_v4 = make_shared<string>(prs_decompress(phosg::load_file("system/item-tables/ItemMagEdit-bb-v4.prs")));
|
||||
auto new_table_v4 = make_shared<MagEvolutionTable>(mag_data_v4, 0x53);
|
||||
auto new_table_v4 = MagEvolutionTable::create(mag_data_v4, Version::BB_V4);
|
||||
|
||||
this->item_parameter_tables = std::move(new_item_parameter_tables);
|
||||
this->item_translation_table = std::move(new_item_translation_table);
|
||||
this->mag_evolution_table_v1_v2 = std::move(new_table_v1_v2);
|
||||
this->mag_evolution_table_v1 = std::move(new_table_v1);
|
||||
this->mag_evolution_table_v2 = std::move(new_table_v2);
|
||||
this->mag_evolution_table_v3 = std::move(new_table_v3);
|
||||
this->mag_evolution_table_v4 = std::move(new_table_v4);
|
||||
}
|
||||
@@ -2196,6 +2239,39 @@ void ServerState::load_dol_files() {
|
||||
this->dol_file_index = make_shared<DOLFileIndex>("system/dol");
|
||||
}
|
||||
|
||||
void ServerState::generate_bb_stream_file() {
|
||||
config_log.info_f("Generating BB stream file");
|
||||
auto sf = std::make_shared<BBStreamFile>();
|
||||
|
||||
auto add_file = [&](const std::string& filename, std::string&& file_data = "") -> void {
|
||||
if (file_data.empty()) {
|
||||
file_data = phosg::load_file("system/blueburst/" + filename);
|
||||
}
|
||||
auto& e = sf->entries.emplace_back();
|
||||
e.size = file_data.size();
|
||||
e.checksum = phosg::crc32(file_data.data(), file_data.size());
|
||||
e.offset = sf->data.size();
|
||||
e.filename = filename;
|
||||
sf->data += file_data;
|
||||
config_log.debug_f(
|
||||
"[BBStreamFile] Added file {} at offset {:08X} ({:08X} bytes) with checksum {:08X}; total size is now {:08X}",
|
||||
filename, e.offset, e.size, e.checksum, sf->data.size());
|
||||
};
|
||||
|
||||
add_file("BattleParamEntry.dat");
|
||||
add_file("BattleParamEntry_on.dat");
|
||||
add_file("BattleParamEntry_lab.dat");
|
||||
add_file("BattleParamEntry_lab_on.dat");
|
||||
add_file("BattleParamEntry_ep4.dat");
|
||||
add_file("BattleParamEntry_ep4_on.dat");
|
||||
add_file("PlyLevelTbl.prs");
|
||||
add_file("ItemMagEdit.prs");
|
||||
auto pmt = this->item_parameter_table(Version::BB_V4);
|
||||
add_file("ItemPMT.prs", prs_compress_optimal(pmt->serialize_binary(Version::BB_V4)));
|
||||
|
||||
this->bb_stream_file = sf;
|
||||
}
|
||||
|
||||
void ServerState::create_default_lobbies() {
|
||||
if (this->default_lobbies_created) {
|
||||
return;
|
||||
@@ -2275,6 +2351,7 @@ void ServerState::load_all(bool enable_thread_pool) {
|
||||
this->load_config_late();
|
||||
this->load_teams();
|
||||
this->load_quest_index();
|
||||
this->generate_bb_stream_file();
|
||||
}
|
||||
|
||||
void ServerState::disconnect_all_banned_clients() {
|
||||
|
||||
+28
-4
@@ -23,6 +23,7 @@
|
||||
#include "ItemTranslationTable.hh"
|
||||
#include "LevelTable.hh"
|
||||
#include "Lobby.hh"
|
||||
#include "MagEvolutionTable.hh"
|
||||
#include "Menu.hh"
|
||||
#include "Quest.hh"
|
||||
#include "TeamIndex.hh"
|
||||
@@ -64,6 +65,17 @@ struct CheatFlags {
|
||||
explicit CheatFlags(const phosg::JSON& json);
|
||||
};
|
||||
|
||||
struct BBStreamFile {
|
||||
struct Entry {
|
||||
uint32_t offset;
|
||||
uint32_t size;
|
||||
uint32_t checksum; // crc32
|
||||
std::string filename;
|
||||
};
|
||||
std::vector<Entry> entries;
|
||||
std::string data;
|
||||
};
|
||||
|
||||
struct ServerState : public std::enable_shared_from_this<ServerState> {
|
||||
enum class RunShellBehavior {
|
||||
DEFAULT = 0,
|
||||
@@ -91,6 +103,7 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
|
||||
std::shared_ptr<const phosg::JSON> config_json;
|
||||
bool one_time_config_loaded = false;
|
||||
bool default_lobbies_created = false;
|
||||
bool is_replay = false;
|
||||
|
||||
size_t num_worker_threads = 0;
|
||||
std::unique_ptr<asio::thread_pool> thread_pool;
|
||||
@@ -155,7 +168,16 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
|
||||
BehaviorSwitch cheat_mode_behavior = BehaviorSwitch::OFF_BY_DEFAULT;
|
||||
bool default_switch_assist_enabled = false;
|
||||
bool use_game_creator_section_id = false;
|
||||
bool enable_bb_ship_selection_menu = false;
|
||||
bool use_psov2_rand_crypt = false; // Used in some tests
|
||||
bool enable_blueballz = false;
|
||||
int64_t blueballz_enemy_hp_scale_tier = -1; // -1 = disabled; 0..10 = scale BB enemy HP in stream files
|
||||
bool enable_hardcore_mode = false;
|
||||
int8_t blueballz_max_tier = 10;
|
||||
int8_t blueballz_unlocked_tier_v2 = 0;
|
||||
int8_t blueballz_unlocked_tier_v3 = 0;
|
||||
int8_t blueballz_unlocked_tier_v4 = 0;
|
||||
bool use_legacy_item_random_behavior = false; // Used in some tests
|
||||
bool rare_notifs_enabled_for_client_drops = false;
|
||||
bool default_rare_notifs_enabled_v1_v2 = false;
|
||||
bool default_rare_notifs_enabled_v3_v4 = false;
|
||||
@@ -177,7 +199,7 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
|
||||
std::unordered_map<uint64_t, std::shared_ptr<const SuperMap>> supermap_for_source_hash_sum;
|
||||
std::unordered_map<uint32_t, std::shared_ptr<const SuperMap>> supermap_for_free_play_key;
|
||||
std::shared_ptr<const RoomLayoutIndex> room_layout_index;
|
||||
std::shared_ptr<FileContentsCache> bb_stream_files_cache;
|
||||
std::shared_ptr<const BBStreamFile> bb_stream_file;
|
||||
std::shared_ptr<FileContentsCache> bb_system_cache;
|
||||
std::shared_ptr<FileContentsCache> gba_files_cache;
|
||||
std::shared_ptr<const DOLFileIndex> dol_file_index;
|
||||
@@ -206,7 +228,8 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
|
||||
std::array<std::shared_ptr<const ItemData::StackLimits>, NUM_VERSIONS> item_stack_limits_tables;
|
||||
size_t bb_max_bank_items = 200;
|
||||
size_t bb_max_bank_meseta = 999999;
|
||||
std::shared_ptr<const MagEvolutionTable> mag_evolution_table_v1_v2;
|
||||
std::shared_ptr<const MagEvolutionTable> mag_evolution_table_v1;
|
||||
std::shared_ptr<const MagEvolutionTable> mag_evolution_table_v2;
|
||||
std::shared_ptr<const MagEvolutionTable> mag_evolution_table_v3;
|
||||
std::shared_ptr<const MagEvolutionTable> mag_evolution_table_v4;
|
||||
std::shared_ptr<const TextIndex> text_index;
|
||||
@@ -308,13 +331,13 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
|
||||
|
||||
std::unordered_map<uint32_t, ProxySession::PersistentConfig> proxy_persistent_configs;
|
||||
|
||||
explicit ServerState(const std::string& config_filename = "");
|
||||
explicit ServerState(const std::string& config_filename = "", bool is_replay = false);
|
||||
ServerState(const ServerState&) = delete;
|
||||
ServerState(ServerState&&) = delete;
|
||||
ServerState& operator=(const ServerState&) = delete;
|
||||
ServerState& operator=(ServerState&&) = delete;
|
||||
|
||||
void add_client_to_available_lobby(std::shared_ptr<Client> c);
|
||||
void add_client_to_available_lobby(std::shared_ptr<Client> c, bool allow_games);
|
||||
void remove_client_from_lobby(std::shared_ptr<Client> c);
|
||||
bool change_client_lobby(
|
||||
std::shared_ptr<Client> c,
|
||||
@@ -438,6 +461,7 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
|
||||
void load_quest_index(bool raise_on_any_failure = false);
|
||||
void compile_functions(bool raise_on_any_failure = false);
|
||||
void load_dol_files();
|
||||
void generate_bb_stream_file();
|
||||
|
||||
void load_all(bool enable_thread_pool);
|
||||
|
||||
|
||||
@@ -211,14 +211,17 @@ ShellCommand c_reload(
|
||||
args.s->load_set_data_tables();
|
||||
} else if (type == "battle-params") {
|
||||
args.s->load_battle_params();
|
||||
args.s->generate_bb_stream_file();
|
||||
} else if (type == "level-tables") {
|
||||
args.s->load_level_tables();
|
||||
args.s->generate_bb_stream_file();
|
||||
} else if (type == "text-index") {
|
||||
args.s->load_text_index();
|
||||
} else if (type == "word-select") {
|
||||
args.s->load_word_select_table();
|
||||
} else if (type == "item-definitions") {
|
||||
args.s->load_item_definitions();
|
||||
args.s->generate_bb_stream_file();
|
||||
} else if (type == "item-name-index") {
|
||||
args.s->load_item_name_indexes();
|
||||
} else if (type == "drop-tables") {
|
||||
|
||||
+15
-5
@@ -293,8 +293,18 @@ uint8_t npc_for_name(const string& name, Version version) {
|
||||
|
||||
const char* name_for_char_class(uint8_t cls) {
|
||||
static const array<const char*, 12> names = {
|
||||
"HUmar", "HUnewearl", "HUcast", "RAmar", "RAcast", "RAcaseal", "FOmarl", "FOnewm", "FOnewearl", "HUcaseal",
|
||||
"FOmar", "RAmarl"};
|
||||
/* 00 */ "HUmar", // 0
|
||||
/* 01 */ "HUnewearl", // 0
|
||||
/* 02 */ "HUcast", // 1
|
||||
/* 03 */ "RAmar", // 0
|
||||
/* 04 */ "RAcast", // 2
|
||||
/* 05 */ "RAcaseal", // 1
|
||||
/* 06 */ "FOmarl", // 0
|
||||
/* 07 */ "FOnewm", // 0
|
||||
/* 08 */ "FOnewearl", // 0
|
||||
/* 09 */ "HUcaseal", // 1
|
||||
/* 0A */ "FOmar", // 0
|
||||
/* 0B */ "RAmarl"}; // 0
|
||||
try {
|
||||
return names.at(cls);
|
||||
} catch (const out_of_range&) {
|
||||
@@ -468,8 +478,8 @@ Language language_for_name(const string& name) {
|
||||
}
|
||||
|
||||
const vector<string> tech_id_to_name = {
|
||||
"foie", "gifoie", "rafoie", "barta", "gibarta", "rabarta", "zonde", "gizonde", "razonde", "grants", "deband",
|
||||
"jellen", "zalure", "shifta", "ryuker", "resta", "anti", "reverser", "megid"};
|
||||
"Foie", "Gifoie", "Rafoie", "Barta", "Gibarta", "Rabarta", "Zonde", "Gizonde", "Razonde", "Grants", "Deband",
|
||||
"Jellen", "Zalure", "Shifta", "Ryuker", "Resta", "Anti", "Reverser", "Megid"};
|
||||
|
||||
const unordered_map<string, uint8_t> name_to_tech_id = {
|
||||
{"foie", 0}, {"gifoie", 1}, {"rafoie", 2}, {"barta", 3}, {"gibarta", 4}, {"rabarta", 5}, {"zonde", 6},
|
||||
@@ -487,7 +497,7 @@ const string& name_for_technique(uint8_t tech) {
|
||||
|
||||
uint8_t technique_for_name(const string& name) {
|
||||
try {
|
||||
return name_to_tech_id.at(name);
|
||||
return name_to_tech_id.at(phosg::tolower(name));
|
||||
} catch (const out_of_range&) {
|
||||
}
|
||||
try {
|
||||
|
||||
@@ -23,6 +23,11 @@
|
||||
__attribute__((packed)); \
|
||||
check_struct_size(StructT, Size)
|
||||
|
||||
#define __packed_ws_be__(StructT, Size) \
|
||||
__attribute__((packed)); \
|
||||
check_struct_size(StructT<false>, Size); \
|
||||
check_struct_size(StructT<true>, Size)
|
||||
|
||||
// Conversion functions
|
||||
|
||||
std::string encode_utf8_char(uint32_t ch);
|
||||
|
||||
@@ -40,12 +40,9 @@ struct NonWindowsRootT {
|
||||
U32T<BE> table4;
|
||||
U32T<BE> article_types_table;
|
||||
U32T<BE> table6;
|
||||
} __attribute__((packed));
|
||||
|
||||
} __packed_ws_be__(NonWindowsRootT, 0x1C);
|
||||
using NonWindowsRoot = NonWindowsRootT<false>;
|
||||
using NonWindowsRootBE = NonWindowsRootT<true>;
|
||||
check_struct_size(NonWindowsRoot, 0x1C);
|
||||
check_struct_size(NonWindowsRootBE, 0x1C);
|
||||
|
||||
struct PCV2Root {
|
||||
le_uint32_t unknown_a1;
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 560 B After Width: | Height: | Size: 2.8 KiB |
Binary file not shown.
@@ -1 +0,0 @@
|
||||
../item-tables/ItemPMT-bb-v4.prs
|
||||
+9
-7
@@ -1,6 +1,8 @@
|
||||
.meta name="Kill count fix"
|
||||
.meta description="Fixes client-side\nkill counts when\nmultiple enemies are\nkilled on the same\nframe"
|
||||
|
||||
.versions 59NJ 59NL
|
||||
|
||||
entry_ptr:
|
||||
reloc0:
|
||||
.offsetof start
|
||||
@@ -9,9 +11,9 @@ start:
|
||||
|
||||
|
||||
|
||||
.data 0x005E32C8
|
||||
.data <VERS 0x005E32A4 0x005E32C8>
|
||||
.deltaof TItemUnitUnsealable_count_kill, TItemUnitUnsealable_count_kill_end
|
||||
.address 0x005E32C8
|
||||
.address <VERS 0x005E32A4 0x005E32C8>
|
||||
TItemUnitUnsealable_count_kill: # [std] (TItemUnitUnsealable* this @ ecx) -> void
|
||||
mov eax, [ecx + 0xF8]
|
||||
movsx eax, word [eax + 0x11A] # eax = this->owner_player->num_kills_since_map_load
|
||||
@@ -29,14 +31,14 @@ TItemUnitUnsealable_count_kill_skip_update:
|
||||
setae dh
|
||||
shl edx, 1
|
||||
or dword [ecx + 0xDC], edx
|
||||
jmp 0x005E2C34
|
||||
jmp <VERS 0x005E2C10 0x005E2C34>
|
||||
TItemUnitUnsealable_count_kill_end:
|
||||
|
||||
|
||||
|
||||
.data 0x005F3EFC
|
||||
.data <VERS 0x005F3E94 0x005F3EFC>
|
||||
.deltaof TItemWeapon_LameDArgent_count_kill, TItemWeapon_LameDArgent_count_kill_end
|
||||
.address 0x005F3EFC
|
||||
.address <VERS 0x005F3E94 0x005F3EFC>
|
||||
TItemWeapon_LameDArgent_count_kill:
|
||||
mov eax, [ecx + 0xF8]
|
||||
movsx eax, word [eax + 0x11A]
|
||||
@@ -59,9 +61,9 @@ TItemWeapon_LameDArgent_count_kill_end:
|
||||
|
||||
|
||||
|
||||
.data 0x005FCA74
|
||||
.data <VERS 0x005FC95C 0x005FCA74>
|
||||
.deltaof TItemWeapon_SealedJSword_count_kill, TItemWeapon_SealedJSword_count_kill_end
|
||||
.address 0x005FCA74
|
||||
.address <VERS 0x005FC95C 0x005FCA74>
|
||||
TItemWeapon_SealedJSword_count_kill:
|
||||
mov eax, [ecx + 0xF8]
|
||||
movsx eax, word [eax + 0x11A]
|
||||
@@ -0,0 +1,624 @@
|
||||
# This patch changes the number of BB character save slots from 4 to any number
|
||||
# up to 127.
|
||||
|
||||
# This patch is for documentation purposes only; it works when used as a server
|
||||
# patch via newserv, but is decidedly inconvenient to use via this method. This
|
||||
# is because it affects logic that runs before any patches can be sent by the
|
||||
# server, so the player has to connect once to get the patch, then disconnect
|
||||
# and connect again to use the additional slots.
|
||||
|
||||
# As written, this patch changes the slot count from 4 to 12. To use a
|
||||
# different slot count, first compute the following values:
|
||||
# slot count = your desired number of player slots (must be >= 4, <= 127)
|
||||
# total file size = (slot count * 0x2EA4) + 0x14
|
||||
# bgm_test_songs_unlocked offset = total file size - 0x10
|
||||
# save_count offset = total file size - 8
|
||||
# round2_seed offset = total file size - 4
|
||||
# Then, for each of the above, search for the string to the left of the = sign
|
||||
# and change the values used in all of the matching lines.
|
||||
|
||||
.meta name="More save slots"
|
||||
.meta description=""
|
||||
.meta hide_from_patches_menu
|
||||
|
||||
entry_ptr:
|
||||
reloc0:
|
||||
.offsetof start
|
||||
|
||||
# Include a few functions first
|
||||
write_call_to_code:
|
||||
.include WriteCallToCode-59NJ
|
||||
memcpy:
|
||||
.include CopyData
|
||||
ret
|
||||
|
||||
|
||||
|
||||
start:
|
||||
# Apply all necessary patches
|
||||
call apply_enable_scroll_patch
|
||||
call apply_fix_scroll_patch1
|
||||
call apply_fix_scroll_patch2
|
||||
call apply_fix_file_index
|
||||
call apply_preview_window_fix
|
||||
call apply_static_patches
|
||||
# Rewrite the existing char file regions to have the appropriate size; this
|
||||
# must be done after the patches are applied because we call the checksum
|
||||
# function, which is patched by one of the above calls
|
||||
call update_existing_char_file_list
|
||||
jmp update_existing_char_file_list_memcard
|
||||
|
||||
|
||||
|
||||
apply_enable_scroll_patch:
|
||||
# This patch enables scrolling behavior within the character list
|
||||
push -5 # Jump size (negative = jmp instead of call)
|
||||
push 0x00413B77 # Jump address
|
||||
call get_code_size_for_enable_scroll
|
||||
.deltaof enable_scroll_start, enable_scroll_end
|
||||
get_code_size_for_enable_scroll:
|
||||
pop eax
|
||||
push dword [eax]
|
||||
call enable_scroll_end
|
||||
enable_scroll_start:
|
||||
mov eax, dword ptr [edi + 0x28] # cursor = char_select_menu->cursor_obj (TAdSelectCurGC*)
|
||||
or dword [eax + 0x01F8], 3 # cursor->flags |= 3 # Enable scrolling
|
||||
mov eax, [0x00A38BD0] # scroll_bar = TAdScrollBarXb_objs[0]
|
||||
mov ecx, [eax + 0xEC] # ecx = scroll_bar->client_id
|
||||
imul ecx, ecx, 0x24
|
||||
# Set up scroll bar graphics (in struct at scroll_bar + 0x1C)
|
||||
mov dword [eax + ecx + 0x1C], 0x439D0000
|
||||
mov dword [eax + ecx + 0x20], 0x43360000
|
||||
mov dword [eax + ecx + 0x24], 0x439D0000
|
||||
mov dword [eax + ecx + 0x28], 0x4392AB85
|
||||
mov dword [eax + ecx + 0x2C], 0x40400000
|
||||
mov dword [eax + ecx + 0x30], 0x425EA3D7
|
||||
mov dword [eax + ecx + 0x34], 0x00000008
|
||||
mov dword [eax + ecx + 0x38], 0x00000000
|
||||
mov dword [eax + ecx + 0x3C], 0x00000000
|
||||
or dword [eax + 0xF0], 1 # scroll_bar->flags |= 1
|
||||
mov ecx, [eax + 0xEC]
|
||||
shl ecx, 4
|
||||
mov dword [eax + ecx + 0xAC], 0 # scroll_bar->selection_state[client_id].scroll_offset = 0
|
||||
mov dword [eax + ecx + 0xB0], 0 # scroll_bar->selection_state[client_id].selected_index = 0
|
||||
mov dword [eax + ecx + 0xB4], 4 # scroll_bar->selection_state[client_id].num_items_in_view = 4
|
||||
mov dword [eax + ecx + 0xB8], 0x0B # scroll_bar->selection_state[client_id].last_item_index = (slot count - 1)
|
||||
pop edi
|
||||
ret
|
||||
enable_scroll_end:
|
||||
call write_call_to_code
|
||||
ret
|
||||
|
||||
|
||||
|
||||
apply_fix_scroll_patch1:
|
||||
# This patch fixes character selection cursor object so it will take the
|
||||
# scroll offset into account
|
||||
push 6 # Call size
|
||||
push 0x00413C30 # Call address
|
||||
call get_code_size_for_fix_scroll_patch1
|
||||
.deltaof fix_scroll_patch1_start, fix_scroll_patch1_end
|
||||
get_code_size_for_fix_scroll_patch1:
|
||||
pop eax
|
||||
push dword [eax]
|
||||
call fix_scroll_patch1_end
|
||||
fix_scroll_patch1_start:
|
||||
mov edx, [edi + 0x28] # cursor = this->ad_select_cur_obj (TAdSelectCurGC*)
|
||||
mov ebp, [edx + 0x44] # ebp = cursor->selected_index_within_view
|
||||
mov eax, [0x00A38BD0] # scroll_bar = TAdScrollBarXb_objs[0]
|
||||
add ebp, [eax + 0xAC] # ebp += scroll_bar->selection_state[0].scroll_offset
|
||||
ret
|
||||
fix_scroll_patch1_end:
|
||||
call write_call_to_code
|
||||
ret
|
||||
|
||||
|
||||
|
||||
apply_fix_scroll_patch2:
|
||||
# This patch changes the TAdSinglePlyChrSelectGC::selected_index_within_view
|
||||
# to be the selected character's absolute index (including scroll_offset),
|
||||
# not the index only within the displayed four characters
|
||||
push 6 # Call size
|
||||
push 0x00413CD0 # Call address
|
||||
call get_code_size_for_fix_scroll_patch2
|
||||
.deltaof fix_scroll_patch2_start, fix_scroll_patch2_end
|
||||
get_code_size_for_fix_scroll_patch2:
|
||||
pop eax
|
||||
push dword [eax]
|
||||
call fix_scroll_patch2_end
|
||||
fix_scroll_patch2_start:
|
||||
mov eax, [0x00A38BD0] # scroll_bar = TAdScrollBarXb_objs[0]
|
||||
mov eax, [eax + 0xAC] # eax = scroll_bar->selection_state[0].scroll_offset
|
||||
mov edx, [edi + 0x28] # cursor = this->ad_select_cur_obj (TAdSelectCurGC*)
|
||||
add eax, [edx + 0x44] # eax += cursor->selected_index_within_view
|
||||
ret
|
||||
fix_scroll_patch2_end:
|
||||
call write_call_to_code
|
||||
ret
|
||||
|
||||
|
||||
|
||||
apply_fix_file_index:
|
||||
# This patch fixes the character file indexing so it will account for the
|
||||
# scroll position
|
||||
push 5 # Call size
|
||||
push 0x00413CE8 # Call address
|
||||
call get_code_size_for_selection_index_fix2
|
||||
.deltaof selection_index_fix2_start, selection_index_fix2_end
|
||||
get_code_size_for_selection_index_fix2:
|
||||
pop eax
|
||||
push dword [eax]
|
||||
call selection_index_fix2_end
|
||||
selection_index_fix2_start:
|
||||
mov eax, [0x00A38BD0]
|
||||
mov eax, [eax + 0xAC] # eax = TAdScrollBarXb_objs[0]->selection_state[0].scroll_offset
|
||||
add ebp, eax # arg0 += eax
|
||||
mov [esp + 4], ebp
|
||||
mov eax, 0x006C1ABC
|
||||
jmp eax # set_current_char_slot
|
||||
selection_index_fix2_end:
|
||||
call write_call_to_code
|
||||
ret
|
||||
|
||||
|
||||
|
||||
apply_preview_window_fix:
|
||||
# This patch fixes the preview display so it will show the correct section
|
||||
# ID, level, etc.
|
||||
push 5 # Call size
|
||||
push 0x0040216C # Call address
|
||||
call get_code_size_for_preview_window_fix
|
||||
.deltaof preview_window_fix_start, preview_window_fix_end
|
||||
get_code_size_for_preview_window_fix:
|
||||
pop eax
|
||||
push dword [eax]
|
||||
call preview_window_fix_end
|
||||
preview_window_fix_start:
|
||||
mov eax, [0x00A38BD0] # scroll_bar = TAdScrollBarXb_objs[0]
|
||||
mov eax, [eax + 0xAC] # eax = scroll_bar->selection_state[0].scroll_offset
|
||||
add [esp + 4], eax
|
||||
mov eax, 0x006C4514 # get_player_preview_info
|
||||
jmp eax
|
||||
preview_window_fix_end:
|
||||
# This patch applies in two places, so push the second set of args now, then
|
||||
# apply it twice
|
||||
push 5 # Call size
|
||||
push 0x00401842 # Call address
|
||||
push dword [esp + 0x10] # Code size
|
||||
push dword [esp + 0x10] # Code address
|
||||
call write_call_to_code
|
||||
call write_call_to_code
|
||||
ret
|
||||
|
||||
|
||||
|
||||
apply_static_patches:
|
||||
.include WriteCodeBlocksBB
|
||||
# These patches change various places where the character data size and slot
|
||||
# count are referenced
|
||||
.data 0x00475294
|
||||
.data 0x00000001
|
||||
.binary 0C # slot count; TDataProtocol::handle_E5
|
||||
.data 0x0047534B
|
||||
.data 0x00000001
|
||||
.binary 0C # slot count; import_player_preview
|
||||
.data 0x004786D1
|
||||
.data 0x00000001
|
||||
.binary 0C # slot count; TDataProtocol::handle_E4
|
||||
.data 0x00482559
|
||||
.data 0x00000004
|
||||
.data 0x00022FC4 # total file size
|
||||
.data 0x006C17FB
|
||||
.data 0x00000001
|
||||
.binary 0C # slot count
|
||||
.data 0x006C1D07
|
||||
.data 0x00000004
|
||||
.data 0x00022FC4 # total file size
|
||||
.data 0x006C1D3A
|
||||
.data 0x00000001
|
||||
.binary 0C # slot count
|
||||
.data 0x006C1D58
|
||||
.data 0x00000004
|
||||
.data 0x00022FC4 # total file size
|
||||
.data 0x006C1E13
|
||||
.data 0x00000004
|
||||
.data 0x00022FC4 # total file size
|
||||
.data 0x006C226A
|
||||
.data 0x00000004
|
||||
.data 0x00022FC4 # total file size
|
||||
.data 0x006C22A9
|
||||
.data 0x00000001
|
||||
.binary 0C # slot count
|
||||
.data 0x006C22CA
|
||||
.data 0x00000004
|
||||
.data 0x00022FC4 # total file size
|
||||
.data 0x006C22DA
|
||||
.data 0x00000004
|
||||
.data 0x00022FC4 # total file size
|
||||
.data 0x006C2517
|
||||
.data 0x00000004
|
||||
.data 0x00022FC4 # total file size
|
||||
.data 0x006C267F
|
||||
.data 0x00000004
|
||||
.data 0x00022FBC # save_count offset
|
||||
.data 0x006C2689
|
||||
.data 0x00000004
|
||||
.data 0x00022FBC # save_count offset
|
||||
.data 0x006C272B
|
||||
.data 0x00000004
|
||||
.data 0x00022FBC # save_count offset
|
||||
.data 0x006C2741
|
||||
.data 0x00000004
|
||||
.data 0x00022FC0 # round2_seed offset
|
||||
.data 0x006C27CF
|
||||
.data 0x00000004
|
||||
.data 0x00022FC4 # total file size
|
||||
.data 0x006C28A8
|
||||
.data 0x00000004
|
||||
.data 0x00022FC4 # total file size
|
||||
.data 0x006C314F
|
||||
.data 0x00000004
|
||||
.data 0x00022FC4 # total file size
|
||||
.data 0x006C357B
|
||||
.data 0x00000004
|
||||
.data 0x00022FC4 # total file size
|
||||
.data 0x006C35BA
|
||||
.data 0x00000001
|
||||
.binary 0C # slot count
|
||||
.data 0x006C35E6
|
||||
.data 0x00000004
|
||||
.data 0x00022FC4 # total file size
|
||||
.data 0x006C35F3
|
||||
.data 0x00000004
|
||||
.data 0x00022FC4 # total file size
|
||||
.data 0x006C360E
|
||||
.data 0x00000004
|
||||
.data 0x00022FBC # save_count offset
|
||||
.data 0x006C3617
|
||||
.data 0x00000004
|
||||
.data 0x00022FBC # save_count offset
|
||||
.data 0x006C371C
|
||||
.data 0x00000004
|
||||
.data 0x00022FC4 # total file size
|
||||
.data 0x006C3B5A
|
||||
.data 0x00000004
|
||||
.data 0x00022FC4 # total file size
|
||||
.data 0x006C424D
|
||||
.data 0x00000004
|
||||
.data 0x00022FC4 # total file size
|
||||
.data 0x006C4833
|
||||
.data 0x00000004
|
||||
.data 0x00022FC4 # total file size
|
||||
.data 0x006C486A
|
||||
.data 0x00000001
|
||||
.binary 0C # slot count
|
||||
.data 0x006C49A6
|
||||
.data 0x00000004
|
||||
.data 0x00022FC4 # total file size
|
||||
.data 0x006C49DD
|
||||
.data 0x00000001
|
||||
.binary 0C # slot count
|
||||
.data 0x006C4AC5
|
||||
.data 0x00000004
|
||||
.data 0x00022FC4 # total file size
|
||||
.data 0x006C4AFE
|
||||
.data 0x00000001
|
||||
.binary 0C # slot count
|
||||
.data 0x006C4CDE
|
||||
.data 0x00000004
|
||||
.data 0x00022FC4 # total file size
|
||||
.data 0x006C4D15
|
||||
.data 0x00000001
|
||||
.binary 0C # slot count
|
||||
.data 0x006C4DFD
|
||||
.data 0x00000004
|
||||
.data 0x00022FC4 # total file size
|
||||
.data 0x006C4E36
|
||||
.data 0x00000001
|
||||
.binary 0C # slot count
|
||||
.data 0x006C4F9C
|
||||
.data 0x00000004
|
||||
.data 0x00022FC4 # total file size
|
||||
.data 0x006C4FD7
|
||||
.data 0x00000001
|
||||
.binary 0C # slot count
|
||||
.data 0x006C51C5
|
||||
.data 0x00000004
|
||||
.data 0x00022FC4 # total file size
|
||||
.data 0x006C5201
|
||||
.data 0x00000001
|
||||
.binary 0C # slot count
|
||||
.data 0x006C5376
|
||||
.data 0x00000004
|
||||
.data 0x00022FC4 # total file size
|
||||
.data 0x006C53B0
|
||||
.data 0x00000001
|
||||
.binary 0C # slot count
|
||||
.data 0x006C5545
|
||||
.data 0x00000004
|
||||
.data 0x00022FC4 # total file size
|
||||
.data 0x006C5581
|
||||
.data 0x00000001
|
||||
.binary 0C # slot count
|
||||
.data 0x006C56F6
|
||||
.data 0x00000004
|
||||
.data 0x00022FC4 # total file size
|
||||
.data 0x006C5730
|
||||
.data 0x00000001
|
||||
.binary 0C # slot count
|
||||
.data 0x006C58B6
|
||||
.data 0x00000004
|
||||
.data 0x00022FC4 # total file size
|
||||
.data 0x006C58F0
|
||||
.data 0x00000001
|
||||
.binary 0C # slot count
|
||||
.data 0x006C5A85
|
||||
.data 0x00000004
|
||||
.data 0x00022FC4 # total file size
|
||||
.data 0x006C5AC1
|
||||
.data 0x00000001
|
||||
.binary 0C # slot count
|
||||
.data 0x006C5BB2
|
||||
.data 0x00000004
|
||||
.data 0x00022FC4 # total file size
|
||||
.data 0x006C5BEC
|
||||
.data 0x00000001
|
||||
.binary 0C # slot count
|
||||
.data 0x006C5D72
|
||||
.data 0x00000004
|
||||
.data 0x00022FC4 # total file size
|
||||
.data 0x006C5DAC
|
||||
.data 0x00000001
|
||||
.binary 0C # slot count
|
||||
.data 0x006C5F32
|
||||
.data 0x00000004
|
||||
.data 0x00022FC4 # total file size
|
||||
.data 0x006C5F6C
|
||||
.data 0x00000001
|
||||
.binary 0C # slot count
|
||||
.data 0x006C60F2
|
||||
.data 0x00000004
|
||||
.data 0x00022FC4 # total file size
|
||||
.data 0x006C612C
|
||||
.data 0x00000001
|
||||
.binary 0C # slot count
|
||||
.data 0x006C6346
|
||||
.data 0x00000004
|
||||
.data 0x00022FC4 # total file size
|
||||
.data 0x006C6381
|
||||
.data 0x00000001
|
||||
.binary 0C # slot count
|
||||
.data 0x006C6505
|
||||
.data 0x00000004
|
||||
.data 0x00022FC4 # total file size
|
||||
.data 0x006C6541
|
||||
.data 0x00000001
|
||||
.binary 0C # slot count
|
||||
.data 0x006C6632
|
||||
.data 0x00000004
|
||||
.data 0x00022FC4 # total file size
|
||||
.data 0x006C666C
|
||||
.data 0x00000001
|
||||
.binary 0C # slot count
|
||||
.data 0x006C67F2
|
||||
.data 0x00000004
|
||||
.data 0x00022FC4 # total file size
|
||||
.data 0x006C682C
|
||||
.data 0x00000001
|
||||
.binary 0C # slot count
|
||||
.data 0x006C69B2
|
||||
.data 0x00000004
|
||||
.data 0x00022FC4 # total file size
|
||||
.data 0x006C69EC
|
||||
.data 0x00000001
|
||||
.binary 0C # slot count
|
||||
.data 0x006C6B87
|
||||
.data 0x00000004
|
||||
.data 0x00022FC4 # total file size
|
||||
.data 0x006C6BB8
|
||||
.data 0x00000004
|
||||
.data 0x0000005D # memcard block count
|
||||
.data 0x006C6C3A
|
||||
.data 0x00000004
|
||||
.data 0x00022FC4 # total file size
|
||||
.data 0x006C6C74
|
||||
.data 0x00000001
|
||||
.binary 0C # slot count
|
||||
.data 0x006C6E82
|
||||
.data 0x00000004
|
||||
.data 0x00022FC4 # total file size
|
||||
.data 0x006C6EBC
|
||||
.data 0x00000001
|
||||
.binary 0C # slot count
|
||||
.data 0x006C70B9
|
||||
.data 0x00000004
|
||||
.data 0x00022FC4 # total file size
|
||||
.data 0x006C70F3
|
||||
.data 0x00000001
|
||||
.binary 0C # slot count
|
||||
.data 0x006C7A46
|
||||
.data 0x00000004
|
||||
.data 0x00022FC4 # total file size
|
||||
.data 0x006C7D66
|
||||
.data 0x00000004
|
||||
.data 0x00022FC4 # total file size
|
||||
.data 0x006C7D7C
|
||||
.data 0x00000001
|
||||
.binary 0C # slot count
|
||||
.data 0x006C7DC0
|
||||
.data 0x00000004
|
||||
.data 0x00022FC4 # total file size
|
||||
.data 0x0077CC72
|
||||
.data 0x00000004
|
||||
.data 0x00022FB4 # bgm_test_songs_unlocked offset
|
||||
|
||||
# Signature check on all save files (rewritten as loop)
|
||||
.data 0x006C1C69
|
||||
.deltaof sig_check_begin, sig_check_end
|
||||
sig_check_begin:
|
||||
mov edx, 0xC87ED5B1 # Expected signature value
|
||||
add eax, 0x04E8 # &char_file_list->chars[0].part2.signature
|
||||
mov ecx, 0x0C # slot count
|
||||
again:
|
||||
cmp dword [eax], 0 # signature == 0 (no char in slot)
|
||||
je sig_ok
|
||||
cmp dword [eax], edx # signature == expected value
|
||||
jne sig_bad
|
||||
sig_ok:
|
||||
add eax, 0x2EA4 # Advance to next slot
|
||||
dec ecx
|
||||
jnz again
|
||||
xor eax, eax # All signatures OK (eax = 0)
|
||||
jmp sig_check_end
|
||||
sig_bad:
|
||||
xor eax, eax # Bad signature (eax = 1)
|
||||
inc eax
|
||||
jmp sig_check_end
|
||||
.binary CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
|
||||
sig_check_end: # 006C1CB2
|
||||
|
||||
# Send slot count in E3 command
|
||||
.data 0x0046EC10 # TDataProtocol::send_E3_for_index
|
||||
.deltaof send_slot_count_in_E3_begin, send_slot_count_in_E3_end
|
||||
send_slot_count_in_E3_begin:
|
||||
# ecx = this (TDataProtocol*)
|
||||
# [esp + 4] = slot_index
|
||||
push 0
|
||||
push dword [esp + 8] # slot_index
|
||||
push 0x0C # slot count
|
||||
push 0x00E30010
|
||||
mov eax, esp
|
||||
push 0x10
|
||||
push eax
|
||||
mov eax, [ecx]
|
||||
call [eax + 0x20] # this->send_command(&cmd, 0x10) // ret 8
|
||||
add esp, 8
|
||||
mov eax, 0x006C1ABC
|
||||
call eax # set_current_char_slot(slot_index) // ret 0
|
||||
add esp, 8
|
||||
ret 4
|
||||
send_slot_count_in_E3_end:
|
||||
|
||||
# Show slot number in each menu item
|
||||
.data 0x00401D57
|
||||
.deltaof show_slot_number_begin, show_slot_number_end
|
||||
show_slot_number_begin:
|
||||
# Original call (sprintf(line_buf, "LV%d", preview_info->visual.disp.level + 1))
|
||||
lea edx, [esp + 0x02C4]
|
||||
mov ebx, [ebx + 8]
|
||||
inc ebx
|
||||
push ebx
|
||||
mov ecx, esi
|
||||
push edx
|
||||
mov eax, 0x00402604
|
||||
call eax
|
||||
# Find the end of the string
|
||||
lea eax, [esp + 0x02C4]
|
||||
show_slot_number_strend_again:
|
||||
cmp word [eax], 0
|
||||
je show_slot_number_strend_done
|
||||
add eax, 2
|
||||
jmp show_slot_number_strend_again
|
||||
show_slot_number_strend_done:
|
||||
# Format the slot number and append it to the string
|
||||
mov ecx, [0x00A38BD0] # scroll_bar = TAdScrollBarXb_objs[0]
|
||||
mov ecx, [ecx + 0xAC] # ecx = scroll_bar->selection_state[0].scroll_offset
|
||||
lea ecx, [ecx + ebp + 1]
|
||||
push ecx # Slot number (scroll_offset + z)
|
||||
call get_show_slot_number_suffix_fmt
|
||||
.binary 20002800230025006400290020000000 # L" (#%d) "
|
||||
get_show_slot_number_suffix_fmt:
|
||||
push eax # Destination buffer
|
||||
mov eax, 0x00835578 # _swprintf
|
||||
call eax
|
||||
add esp, 0x0C
|
||||
jmp show_slot_number_end
|
||||
.zero 0x96
|
||||
show_slot_number_end: # 00401E4D
|
||||
|
||||
# End static patches
|
||||
.data 0x00000000
|
||||
.data 0x00000000
|
||||
|
||||
|
||||
|
||||
update_existing_char_file_list:
|
||||
# Replace the existing character list with an appropriately-longer one. This
|
||||
# part does not need to be done if the patch is applied statically to the
|
||||
# executable; this is only necessary when used as a server patch because the
|
||||
# character list is already allocated at the time the patch is applied.
|
||||
push 0x00022FC4 # total file size
|
||||
mov eax, 0x00835915 # operator_new
|
||||
call eax
|
||||
add esp, 4
|
||||
mov edx, [0x00A939C4] # edx = old char_file_list
|
||||
mov [0x00A939C4], eax
|
||||
mov ecx, [edx + 0xBA94] # Copy bgm_test_songs_unlocked_high to new file
|
||||
mov [eax + 0x00022FB4], ecx
|
||||
mov ecx, [edx + 0xBA98] # Copy bgm_test_songs_unlocked_low to new file
|
||||
mov [eax + 0x00022FB8], ecx
|
||||
mov ecx, [edx + 0xBA9C] # Copy save_count to new file
|
||||
mov [eax + 0x00022FBC], ecx
|
||||
mov ecx, [edx + 0xBAA0] # Copy round2_seed to new file
|
||||
mov [eax + 0x00022FC0], ecx
|
||||
add eax, 4
|
||||
add edx, 4
|
||||
mov ecx, 0xBA90
|
||||
call memcpy # Copy the existing 4 characters over
|
||||
mov eax, [0x00A939C4]
|
||||
add eax, 0xBA94
|
||||
mov ecx, 4
|
||||
clear_next_char:
|
||||
cmp ecx, 0x0C # slot count
|
||||
jge clear_next_char_done
|
||||
lea edx, [eax + 0x2EA4] # edx = ptr to next char (or footer)
|
||||
clear_next_char_write_again:
|
||||
mov dword [eax], 0
|
||||
add eax, 4
|
||||
cmp eax, edx
|
||||
jl clear_next_char_write_again
|
||||
clear_next_char_done:
|
||||
|
||||
# Call eh_vector_constructor_iterator(
|
||||
# &char_file_list.chars[4],
|
||||
# sizeof(char_file_list.chars[0]),
|
||||
# countof(char_file_list.chars) - 4,
|
||||
# PSOCharacterFile::init,
|
||||
# PSOCharacterFile::destroy)
|
||||
push 0x006C197C # PSOCharacterFile::destroy
|
||||
push 0x006C182C # PSOCharacterFile::init
|
||||
push 0x08 # slot count - 4
|
||||
push 0x2EA4 # sizeof(PSOCharacterFile)
|
||||
mov eax, [0x00A939C4]
|
||||
add eax, 0xBA94
|
||||
push eax
|
||||
mov eax, 0x00835E86
|
||||
call eax
|
||||
|
||||
# Fix the file's checksum
|
||||
mov eax, [0x00A939C4]
|
||||
mov ecx, 0x006C2738
|
||||
jmp ecx # PSOBBCharacterFileList::checksum(char_file_list)
|
||||
|
||||
|
||||
|
||||
update_existing_char_file_list_memcard:
|
||||
# Allocate a new memory card file area and copy the data there too. It seems
|
||||
# Sega didn't fully strip out the local saving code from PSOBB; instead, they
|
||||
# just made it write to a heap-allocated buffer. Since the file is much
|
||||
# bigger now, we also have to make that heap-allocated buffer larger. We add
|
||||
# a few "blocks" on the end, since the original code in the game does that
|
||||
# too, but it's probably not strictly necessary.
|
||||
# Like the above, this part is not necessary if this patch is statically
|
||||
# applied to the executable.
|
||||
mov eax, 0x00022FC4 # total file size
|
||||
add eax, 0x0000FFFF
|
||||
and eax, 0xFFFFC000
|
||||
push eax
|
||||
mov eax, 0x0084F258
|
||||
call eax # malloc10(total file size)
|
||||
add esp, 4
|
||||
mov [0x00A939AC], eax
|
||||
mov edx, [0x00A939C4]
|
||||
mov ecx, 0x00022FC4 # total file size
|
||||
jmp memcpy
|
||||
@@ -0,0 +1,103 @@
|
||||
# This patch causes the client not to generate its own EXP text and instead use
|
||||
# the EXP values generated by the server when showing the purple text for enemy
|
||||
# deaths. This makes EXP gained via EXP share visible, as well as makes
|
||||
# fractional EXP multiplers (in config.json) display properly.
|
||||
|
||||
.meta name="Server EXP display"
|
||||
.meta description=""
|
||||
.meta hide_from_patches_menu
|
||||
|
||||
entry_ptr:
|
||||
reloc0:
|
||||
.offsetof start
|
||||
start:
|
||||
call install_hook
|
||||
call apply_static_patches
|
||||
ret
|
||||
|
||||
|
||||
|
||||
install_hook:
|
||||
pop ecx
|
||||
push 0 # Write address instead of a call/jmp opcode
|
||||
push 0x00A0DC54
|
||||
call get_code_size
|
||||
.deltaof handle_6xBF_start, handle_6xBF_end
|
||||
get_code_size:
|
||||
pop eax
|
||||
push dword [eax]
|
||||
call handle_6xBF_end
|
||||
handle_6xBF_start: # [std](G_6xBF* cmd @ [esp + 4]) -> void
|
||||
mov edx, [esp + 4]
|
||||
|
||||
mov ecx, [0x00A9A074] # local_client_id
|
||||
cmp [edx + 2], cx
|
||||
jne skip_text
|
||||
|
||||
cmp byte [edx + 1], 3
|
||||
jl skip_text
|
||||
movzx eax, word [edx + 8] # cmd.from_enemy_id
|
||||
cmp eax, 0x1000
|
||||
jl skip_text
|
||||
cmp eax, 0x1B50
|
||||
jge skip_text
|
||||
call get_enemy_entity
|
||||
|
||||
test eax, eax
|
||||
jnz enemy_entity_ok
|
||||
|
||||
# Use player entity if enemy entity is already gone
|
||||
mov eax, 0x0068D618
|
||||
xchg eax, ecx
|
||||
call ecx # eax = TObjPlayer::for_client_id(local_client_id); conveniently, this function preserves all regs except eax
|
||||
|
||||
enemy_entity_ok:
|
||||
push 0x0000FFFF # entity_id; ignored by TFontSmallTask if not a player
|
||||
push dword [edx + 4] # amount = cmd.amount
|
||||
push 0x00976380 # prefix = L"EXP"
|
||||
push 0x14
|
||||
push 0x14
|
||||
push 0xFFFF00FF # color (ARGB)
|
||||
add eax, 0x300
|
||||
push eax # position
|
||||
mov eax, 0x0078B8E8
|
||||
call eax # TFontSmallTask___new__(...)
|
||||
add esp, 0x1C
|
||||
|
||||
skip_text:
|
||||
mov eax, 0x0069292C # Original handle_6xBF
|
||||
jmp eax # original_handle_6xBF(cmd)
|
||||
|
||||
get_enemy_entity:
|
||||
.include GetEnemyEntity-59NJ
|
||||
ret
|
||||
|
||||
handle_6xBF_end:
|
||||
push ecx
|
||||
.include WriteCallToCode-59NJ
|
||||
|
||||
|
||||
|
||||
apply_static_patches:
|
||||
.include WriteCodeBlocksBB
|
||||
|
||||
.data 0x0078827D
|
||||
.deltaof disable_kill_enemy_callsite_start, disable_kill_enemy_callsite_end
|
||||
disable_kill_enemy_callsite_start:
|
||||
nop
|
||||
nop
|
||||
nop
|
||||
nop
|
||||
nop
|
||||
disable_kill_enemy_callsite_end:
|
||||
|
||||
.data 0x00777381
|
||||
.deltaof disable_exp_steal_callsite_start, disable_exp_steal_callsite_end
|
||||
disable_exp_steal_callsite_start:
|
||||
add esp, 0x0C # Original function has `ret 0x0C`
|
||||
nop
|
||||
nop
|
||||
disable_exp_steal_callsite_end:
|
||||
|
||||
.data 0x00000000
|
||||
.data 0x00000000
|
||||
+5
-3
@@ -10,6 +10,8 @@
|
||||
.meta description=""
|
||||
.meta hide_from_patches_menu
|
||||
|
||||
.versions 59NJ 59NL
|
||||
|
||||
entry_ptr:
|
||||
reloc0:
|
||||
.offsetof start
|
||||
@@ -17,7 +19,7 @@ start:
|
||||
.include WriteCodeBlocksBB
|
||||
|
||||
# Patch 1: rewrite item_is_stackable
|
||||
.data 0x005C502C
|
||||
.data <VERS 0x005C5020 0x005C502C>
|
||||
.deltaof item_is_stackable_start, item_is_stackable_end
|
||||
|
||||
item_is_stackable_start:
|
||||
@@ -32,7 +34,7 @@ item_is_stackable_start:
|
||||
push eax
|
||||
mov ecx, esp
|
||||
|
||||
.binary E8EC130100 # call max_stack_size_for_tool_start
|
||||
.binary <VERS E8D8130100 E8EC130100> # call max_stack_size_for_tool_start
|
||||
pop ecx
|
||||
cmp eax, 1
|
||||
jg return_1
|
||||
@@ -48,7 +50,7 @@ return_1:
|
||||
item_is_stackable_end:
|
||||
|
||||
# Patch 2: rewrite max_stack_size_for_tool
|
||||
.data 0x005D6430
|
||||
.data <VERS 0x005D6410 0x005D6430>
|
||||
.deltaof max_stack_size_for_tool_start, max_stack_size_for_tool_end
|
||||
|
||||
max_stack_size_for_tool_start:
|
||||
@@ -18,6 +18,7 @@ start:
|
||||
|
||||
|
||||
# Tiny Grass Assassins Bug Fix
|
||||
|
||||
.data <VERS 0x0016227A 0x0016238A 0x0016232A 0x0016240A 0x0016229A 0x0016242A 0x0016225A>
|
||||
.data 0x00000002
|
||||
.binary EB0E
|
||||
@@ -25,6 +26,7 @@ start:
|
||||
|
||||
|
||||
# Shield DFP/EVP Bug Fix (allows shields to reach true max DFP/EVP values)
|
||||
|
||||
.data <VERS 0x00185D8E 0x00185F4E 0x0018600E 0x00185F0E 0x00185F6E 0x00185F2E 0x00185F2E>
|
||||
.data 0x00000001
|
||||
.binary 16
|
||||
@@ -35,6 +37,7 @@ start:
|
||||
|
||||
|
||||
# VR Spaceship Item Drop Bug Fix (allows items to drop from enemies above a certain Y position)
|
||||
|
||||
.data <VERS 0x00175D75 0x00175E55 0x00175F35 0x00175EC5 0x00175ED5 0x00175EE5 0x00175E95>
|
||||
.data 0x00000002
|
||||
.data 0x435C0000
|
||||
@@ -42,6 +45,7 @@ start:
|
||||
|
||||
|
||||
# Gol Dragon Camera Bug Fix (makes the camera after Gol Dragon display "normally")
|
||||
|
||||
.data <VERS 0x000A8AE1 0x000A8C51 0x000A8BD1 0x000A89C1 0x000A8961 0x000A89E1 0x000A8921>
|
||||
.data 0x00000002
|
||||
.binary 01
|
||||
@@ -49,6 +53,7 @@ start:
|
||||
|
||||
|
||||
# Rain Drops Color Bug Fix
|
||||
|
||||
.data <VERS 0x0054D670 0x0054DD00 0x005557E8 0x00552C68 0x00552508 0x00552C68 0x00553008>
|
||||
.data 0x00000008
|
||||
.binary 7080808060707070
|
||||
@@ -56,6 +61,7 @@ start:
|
||||
|
||||
|
||||
# TP Bar Color Bug Fix
|
||||
|
||||
.data <VERS 0x002779CE 0x00277C7E 0x0027808E 0x00277DAE 0x00277ECE 0x00277DCE 0x00277F9E>
|
||||
.data 0x00000004
|
||||
.data 0xFF00AAFA
|
||||
@@ -71,13 +77,116 @@ start:
|
||||
|
||||
|
||||
|
||||
# Olga Flow Barta Bug Fix
|
||||
|
||||
.label g1_hook_call, <VERS 0x000970E0 0x000973F0 0x00097460 0x00097140 0x000970E0 0x00097160 0x00096FE0>
|
||||
.label g1_hook_loc, <VERS 0x00097124 0x00097434 0x000974A4 0x00097184 0x00097124 0x000971A4 0x00097024>
|
||||
.data g1_hook_call
|
||||
.data 6
|
||||
.address g1_hook_call
|
||||
mov eax, esi
|
||||
cmp al, 19
|
||||
jmp g1_hook_loc
|
||||
g1_hook_call_end:
|
||||
.data g1_hook_loc
|
||||
.deltaof g1_hook_start, g1_hook_end
|
||||
.address g1_hook_loc
|
||||
g1_hook_start:
|
||||
jne g1_hook_skip_replace_value
|
||||
mov al, 2
|
||||
g1_hook_skip_replace_value:
|
||||
cmp eax, [ebx + 0x440] // Original opcode
|
||||
jmp g1_hook_call_end
|
||||
g1_hook_end:
|
||||
|
||||
|
||||
|
||||
# Morfos Frozen Player Bug Fix
|
||||
|
||||
.label g2_hook_call, <VERS 0x0012E257 0x0012E387 0x0012E4E7 0x0012E537 0x0012E567 0x0012E557 0x0012E5A7>
|
||||
.label g2_hook_loc1, <VERS 0x0012E5F4 0x0012E724 0x0012E884 0x0012E8D4 0x0012E904 0x0012E8F4 0x0012E944>
|
||||
.label g2_hook_loc2, <VERS 0x0012E622 0x0012E752 0x0012E8B2 0x0012E902 0x0012E932 0x0012E922 0x0012E972>
|
||||
.data g2_hook_call
|
||||
.data 6
|
||||
.address g2_hook_call
|
||||
call g2_hook_loc1
|
||||
nop
|
||||
.data g2_hook_loc1
|
||||
.deltaof g2_hook_start1, g2_hook_end1
|
||||
.address g2_hook_loc1
|
||||
g2_hook_start1:
|
||||
fld1 st0 // st = [1.0, speed]
|
||||
fld1 st0 // st = [1.0, 1.0, speed]
|
||||
fadd st0, st1 // st = [2.0, 1.0, speed]
|
||||
fdivp st1, st0 // st = [0.5, speed]
|
||||
jmp g2_hook_loc2
|
||||
g2_hook_end1:
|
||||
|
||||
.data g2_hook_loc2
|
||||
.deltaof g2_hook_start2, g2_hook_end2
|
||||
.address g2_hook_loc2
|
||||
g2_hook_start2:
|
||||
test byte [esi + 0x30], 0x20 // If not set, use 1.5; if set, use 0.5
|
||||
jnz g2_hook_entity_is_frozen
|
||||
fld1 st0 // st = [1, 0.5, speed]
|
||||
faddp st1, st0 // st = [1.5, speed]
|
||||
g2_hook_entity_is_frozen:
|
||||
fmulp st1, st0 // st = [((game_flags & 0x20) ? 0.5 : 1.5) * speed]
|
||||
ret
|
||||
g2_hook_end2:
|
||||
|
||||
|
||||
|
||||
# Dropped Mag Color Bug Fix (only needed on beta version)
|
||||
|
||||
.only_versions 4OJB
|
||||
.data 0x001759E6
|
||||
.data 1
|
||||
.binary 12
|
||||
.data 0x00180898
|
||||
.data 1
|
||||
.binary 12
|
||||
.all_versions
|
||||
|
||||
|
||||
|
||||
# Box/Fence Fadeout Bug Fix
|
||||
|
||||
.data <VERS 0x001D229B 0x001D244B 0x001D295B 0x001D241B 0x001D26AB 0x001D243B 0x001D26DB>
|
||||
.data 2
|
||||
nop
|
||||
nop
|
||||
|
||||
.data <VERS 0x001DF7C4 0x001DF924 0x001DFD94 0x001DF964 0x001DFB04 0x001DF984 0x001DFA74>
|
||||
.data 6
|
||||
nop
|
||||
nop
|
||||
nop
|
||||
nop
|
||||
nop
|
||||
nop
|
||||
|
||||
|
||||
|
||||
# TODO: Port the rest of the patches in the GC version of BugFixes:
|
||||
|
||||
# Olga Flow Barta Bug Fix
|
||||
# Morfos Frozen Player Bug Fix
|
||||
# Bulclaw HP Bug Fix
|
||||
# Control Tower: Delbiter Death SFX Bug Fix
|
||||
# Weapon Attributes Patch
|
||||
# Invalid Items Bug Fix
|
||||
# Item Removal Maxed Stats Bug Fix
|
||||
# Unit Present Bug Fix
|
||||
# Bank Item Stacking Bug Fix
|
||||
# Meseta Drop System Bug Fix
|
||||
# Offline Quests Drop Table Bug Fix
|
||||
# Mag Revival Priority Bug Fix
|
||||
# Mag Revival Challenge & Quest Mode Bug Fix
|
||||
# Reverser Target Lock Bug Fix
|
||||
# Deband/Shifta/Resta Target Bug Fix
|
||||
# Tech Auto Targeting Bug Fix
|
||||
# Enable Trap Animations
|
||||
# Tsumikiri J-Sword special attack + rapid weapon switch bug fix
|
||||
|
||||
# Control Tower: Delbiter Death SFX Bug Fix
|
||||
# Ruins Laser Fence SFX Bug Fix
|
||||
# SFX Cancellation Distance Bug Fix
|
||||
# Foie SFX Pitch Bug Fix
|
||||
@@ -92,28 +201,12 @@ start:
|
||||
# Grants SFX Pitch Bug Fix
|
||||
# Megid SFX Pitch Bug Fix
|
||||
# Anti SFX Pitch Bug Fix
|
||||
# Invalid Items Bug Fix
|
||||
# Item Removal Maxed Stats Bug Fix
|
||||
# Unit Present Bug Fix
|
||||
# Bank Item Stacking Bug Fix
|
||||
# Dropped Mag Color Bug Fix
|
||||
# Meseta Drop System Bug Fix
|
||||
# Present Color Bug Fix
|
||||
# Offline Quests Drop Table Bug Fix
|
||||
# Mag Revival Priority Bug Fix
|
||||
# Mag Revival Challenge & Quest Mode Bug Fix
|
||||
# Chat Bubble Window TAB Bug Fix
|
||||
# Chat Log Window LF/Tab Bug Fix
|
||||
# Dark/Hell Special GFX Bug Fix
|
||||
# Box/Fence Fadeout Bug Fix
|
||||
# Devil's and Demon's Special Damage Display Bug Fix
|
||||
# Christmas Trees Bug Fix
|
||||
# Reverser Target Lock Bug Fix
|
||||
# Deband/Shifta/Resta Target Bug Fix
|
||||
# Tech Auto Targeting Bug Fix
|
||||
# Enable Trap Animations
|
||||
# Tsumikiri J-Sword special attack + rapid weapon switch bug fix
|
||||
|
||||
|
||||
|
||||
.data 0x00000000
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
.meta hide_from_patches_menu
|
||||
.meta name="Player flags"
|
||||
.meta description=""
|
||||
|
||||
entry_ptr:
|
||||
reloc0:
|
||||
.offsetof start
|
||||
start:
|
||||
.include WriteCodeBlocksGC
|
||||
|
||||
.label check_controller_button, 0x801A6C68 # [std](ControllerState* st, uint32_t flags) -> bool
|
||||
.label TFogCtrl_change_fog, 0x800FB10C # [std](TFogCtrl* this, uint32_t fog_num, uint32_t instant_transition) -> void
|
||||
.label render_debug_printf, 0x803D4E3C # [std](uint32_t coords, const char* fmt, ...) -> void
|
||||
.label set_debug_text_color, 0x803D4990 # [](uint32_t color_argb) -> void
|
||||
.label hook_call, 0x80228A38
|
||||
.label hook_loc, 0x8000A000
|
||||
|
||||
# Disable D-pad up + down chat shortcuts
|
||||
.data 0x80246314
|
||||
.data 4
|
||||
li r4, 0
|
||||
.data 0x80246330
|
||||
.data 4
|
||||
li r4, 0
|
||||
|
||||
.data hook_call
|
||||
.data 4
|
||||
.address hook_call
|
||||
b hook_loc
|
||||
|
||||
.data hook_loc
|
||||
.deltaof hook_start, hook_end
|
||||
.address hook_loc
|
||||
hook_start:
|
||||
mflr r0
|
||||
stwu [r1 - 0x20], r1
|
||||
stw [r1 + 0x24], r0
|
||||
stw [r1 + 0x08], r30
|
||||
stw [r1 + 0x0C], r31
|
||||
|
||||
lis r30, 0x804F
|
||||
ori r30, r30, 0xC7A8
|
||||
lwz r4, [r13 - 0x5280] # local_client_id
|
||||
rlwinm r4, r4, 2, 0, 29
|
||||
lwzx r30, [r30 + r4] # r30 = TFogCtrl_for_client_id[local_client_id]
|
||||
|
||||
cmpwi r30, 0
|
||||
beq hook_skip_all
|
||||
|
||||
lwz r31, [r30 + 0x0184] # Active slot number
|
||||
rlwinm r31, r31, 2, 0, 29
|
||||
addi r31, r31, 0x174
|
||||
lwzx r31, [r30 + r31] # Active slot pointer
|
||||
lwz r31, [r31 + 0x40] # Active fog number
|
||||
|
||||
# Check for button presses to change fog
|
||||
lis r3, 0x8050
|
||||
ori r3, r3, 0x9848
|
||||
li r4, 0x0010 # D-pad up
|
||||
bl check_controller_button
|
||||
cmplwi r3, 0
|
||||
beq hook_skip_incr_fog
|
||||
mr r3, r30
|
||||
addi r4, r31, 1
|
||||
andi. r4, r4, 0x007F
|
||||
li r5, 1
|
||||
bl TFogCtrl_change_fog
|
||||
hook_skip_incr_fog:
|
||||
lis r3, 0x8050
|
||||
ori r3, r3, 0x9848
|
||||
li r4, 0x0020 # D-pad down
|
||||
bl check_controller_button
|
||||
cmplwi r3, 0
|
||||
beq hook_skip_decr_fog
|
||||
mr r3, r30
|
||||
subi r4, r31, 1
|
||||
andi. r4, r4, 0x007F
|
||||
li r5, 1
|
||||
bl TFogCtrl_change_fog
|
||||
hook_skip_decr_fog:
|
||||
|
||||
# Show the current fog number
|
||||
lis r3, 0xFFFF
|
||||
ori r3, r3, 0x00FF
|
||||
bl set_debug_text_color
|
||||
lis r3, 0x0002
|
||||
ori r3, r3, 0x000B
|
||||
bl hook_get_fmt_string
|
||||
.binary "Fog: %02X"000000
|
||||
hook_get_fmt_string:
|
||||
mflr r4
|
||||
mr r5, r31
|
||||
bl render_debug_printf
|
||||
|
||||
hook_skip_all:
|
||||
lwz r31, [r1 + 0x0C]
|
||||
lwz r30, [r1 + 0x08]
|
||||
lwz r0, [r1 + 0x24]
|
||||
addi r1, r1, 0x20
|
||||
mtlr r0
|
||||
blr
|
||||
|
||||
hook_end:
|
||||
|
||||
.data 0
|
||||
.data 0
|
||||
@@ -0,0 +1,74 @@
|
||||
.meta hide_from_patches_menu
|
||||
.meta name="Player flags"
|
||||
.meta description=""
|
||||
|
||||
entry_ptr:
|
||||
reloc0:
|
||||
.offsetof start
|
||||
start:
|
||||
.include WriteCodeBlocksGC
|
||||
|
||||
.label TObjPlayer_for_client_id, 0x801BA59C # [std](uint32_t client_id)
|
||||
.label render_debug_printf, 0x803D4E3C # [std](uint32_t coords, const char* fmt, ...);
|
||||
.label set_debug_text_color, 0x803D4990 # [](uint32_t color_argb);
|
||||
.label hook_call, 0x80228A38
|
||||
.label hook_loc, 0x8000A000
|
||||
|
||||
.data hook_call
|
||||
.data 4
|
||||
.address hook_call
|
||||
b hook_loc
|
||||
|
||||
.data hook_loc
|
||||
.deltaof hook_start, hook_end
|
||||
.address hook_loc
|
||||
hook_start:
|
||||
mflr r0
|
||||
stwu [r1 - 0x20], r1
|
||||
stw [r1 + 0x24], r0
|
||||
stw [r1 + 8], r30
|
||||
li r30, 0
|
||||
|
||||
hook_again:
|
||||
li r6, 0
|
||||
mr r3, r30
|
||||
bl TObjPlayer_for_client_id
|
||||
cmplwi r3, 0
|
||||
beq hook_skip_player
|
||||
lwz r6, [r3 + 0x0334] # player_flags
|
||||
lwz r4, [r13 - 0x5280] # local_client_id
|
||||
cmp r4, r30
|
||||
bne hook_not_local_player
|
||||
lis r3, 0xFFFF
|
||||
ori r3, r3, 0x00FF
|
||||
b hook_player_flags_ok
|
||||
hook_not_local_player:
|
||||
lis r3, 0xFFFF
|
||||
ori r3, r3, 0xFFFF
|
||||
hook_player_flags_ok:
|
||||
bl set_debug_text_color
|
||||
|
||||
lis r3, 0x0002
|
||||
ori r3, r3, 0x000B
|
||||
add r3, r3, r30
|
||||
bl hook_get_fmt_string
|
||||
.binary "Player %2d: %08X"00000000
|
||||
hook_get_fmt_string:
|
||||
mflr r4
|
||||
mr r5, r30
|
||||
bl render_debug_printf
|
||||
|
||||
hook_skip_player:
|
||||
addi r30, r30, 1
|
||||
cmplwi r30, 0x0C
|
||||
blt hook_again
|
||||
|
||||
lwz r30, [r1 + 8]
|
||||
lwz r0, [r1 + 0x24]
|
||||
addi r1, r1, 0x20
|
||||
mtlr r0
|
||||
blr
|
||||
hook_end:
|
||||
|
||||
.data 0
|
||||
.data 0
|
||||
@@ -0,0 +1,146 @@
|
||||
# Currently beta quality, map objects that fade like boxes, and Pioneer's
|
||||
# background billboards and elevators still have regular draw distance.
|
||||
# TODO: 90% of stuff is included, bring home the last 10%.
|
||||
|
||||
.meta name="Draw Distance"
|
||||
.meta description="Extends the draw\ndistance of many\nobjects"
|
||||
|
||||
entry_ptr:
|
||||
reloc0:
|
||||
.offsetof start
|
||||
|
||||
write_call_func:
|
||||
.include WriteCallToCode-59NJ
|
||||
|
||||
start:
|
||||
mov eax, 0x41800000 # Environment clip distance mod 16.0f
|
||||
mov [0x0097D198], eax # This affects mostly static map objects
|
||||
mov [0x0097D19C], eax
|
||||
mov [0x00097D1A0], eax
|
||||
|
||||
mov ax, 0x9090
|
||||
mov [0x00689BC7], ax # Players draw distance 10000.0f always
|
||||
mov eax, 0x41000000 # Use newly acquired skipped branch room
|
||||
mov [0x00689BD1], eax # to store our float multiplier
|
||||
|
||||
call patch_func_1 # Floor items
|
||||
call patch_func_2 # Whole bunch of stuff, including NPCs
|
||||
call patch_func_3 # Duplicate function from above, reuse same hook
|
||||
call patch_func_4 # TODO: Which objects this affects?
|
||||
call patch_func_5 # TODO: This one too?
|
||||
call patch_func_6 # TODO: And this one?
|
||||
ret
|
||||
|
||||
# Floor items
|
||||
patch_func_1:
|
||||
pop ecx
|
||||
push 8
|
||||
push 0x005C525B
|
||||
call get_code_size1
|
||||
.deltaof patch_code1, patch_code_end1
|
||||
get_code_size1:
|
||||
pop eax
|
||||
push dword [eax]
|
||||
call patch_code_end1
|
||||
patch_code1:
|
||||
mov edx, [esp + 0x18]
|
||||
fld st0, dword [0x00689BD1]
|
||||
fld st0, dword [esp + 0x14]
|
||||
fmulp st1, st0
|
||||
ret
|
||||
patch_code_end1:
|
||||
push ecx
|
||||
jmp write_call_func
|
||||
|
||||
# Whole bunch of stuff, including NPCs
|
||||
patch_func_2:
|
||||
pop ecx
|
||||
push 9
|
||||
push 0x007BB21E
|
||||
call get_code_size2
|
||||
.deltaof patch_code2, patch_code_end2
|
||||
get_code_size2:
|
||||
pop eax
|
||||
push dword [eax]
|
||||
call patch_code_end2
|
||||
patch_code2:
|
||||
test eax, 0x400
|
||||
fld st0, dword [0x00689BD1]
|
||||
fld st0, dword [esp + 0x2C]
|
||||
fmulp st1, st0
|
||||
ret
|
||||
patch_code_end2:
|
||||
push ecx
|
||||
jmp write_call_func
|
||||
|
||||
# Duplicate function from above, reuse same hook
|
||||
patch_func_3:
|
||||
mov eax, dword [0x007BB21F]
|
||||
add eax, 0x002A1C74
|
||||
mov dword [0x00518843], eax
|
||||
mov byte [0x00518842], 0xE8
|
||||
mov dword [0x00518847], 0x90909090
|
||||
ret
|
||||
|
||||
# TOComputerMachine01
|
||||
patch_func_4:
|
||||
pop ecx
|
||||
push 7
|
||||
push 0x00616FF4
|
||||
call get_code_size4
|
||||
.deltaof patch_code4, patch_code_end4
|
||||
get_code_size4:
|
||||
pop eax
|
||||
push dword [eax]
|
||||
call patch_code_end4
|
||||
patch_code4:
|
||||
lea edx, [edi + 0x38]
|
||||
fld st0, dword [0x00689BD1]
|
||||
fld st0, dword [esp + 0x14]
|
||||
fmulp st1, st0
|
||||
ret
|
||||
patch_code_end4:
|
||||
push ecx
|
||||
jmp write_call_func
|
||||
|
||||
# TObjCamera
|
||||
patch_func_5:
|
||||
pop ecx
|
||||
push 6
|
||||
push 0x006439A8
|
||||
call get_code_size5
|
||||
.deltaof patch_code5, patch_code_end5
|
||||
get_code_size5:
|
||||
pop eax
|
||||
push dword [eax]
|
||||
call patch_code_end5
|
||||
patch_code5:
|
||||
fld st0, dword [0x00689BD1]
|
||||
fld st0, dword [esp + 0x28]
|
||||
fmulp st1, st0
|
||||
fchs st0
|
||||
ret
|
||||
patch_code_end5:
|
||||
push ecx
|
||||
jmp write_call_func
|
||||
|
||||
# TODO: And this one?
|
||||
patch_func_6:
|
||||
pop ecx
|
||||
push 6
|
||||
push 0x0065B959
|
||||
call get_code_size6
|
||||
.deltaof patch_code6, patch_code_end6
|
||||
get_code_size6:
|
||||
pop eax
|
||||
push dword [eax]
|
||||
call patch_code_end6
|
||||
patch_code6:
|
||||
mov ebp, ecx
|
||||
fld st0, dword [0x00689BD1]
|
||||
fld st0, dword [esp + 0x30]
|
||||
fmulp st1, st0
|
||||
ret
|
||||
patch_code_end6:
|
||||
push ecx
|
||||
jmp write_call_func
|
||||
@@ -0,0 +1,258 @@
|
||||
.meta name="DMC"
|
||||
.meta description="Mitigates effects\nof enemy health\ndesync"
|
||||
.meta client_flag="0x0000001000000000"
|
||||
|
||||
entry_ptr:
|
||||
reloc0:
|
||||
.offsetof start
|
||||
|
||||
write_call_to_code_multi:
|
||||
.include WriteCallToCodeMulti-59NJ
|
||||
write_address_of_code:
|
||||
.include WriteAddressOfCode-59NJ
|
||||
|
||||
start:
|
||||
|
||||
# Replace 6x09 with 6xE4 in subcommand handler table
|
||||
mov dword [0x00A0DC30], 0x000600E4 # subcommand=0xE4, flags=6
|
||||
push 0x00A0DC34
|
||||
call +4
|
||||
.deltaof handle_6xE4_start, handle_6xE4_end
|
||||
pop eax
|
||||
push dword [eax]
|
||||
call handle_6xE4_end
|
||||
|
||||
handle_6xE4_start: # (G_6xE4* cmd @ [esp + 4]) -> void
|
||||
push ebx
|
||||
push esi
|
||||
push edi
|
||||
|
||||
test byte [0x00AA8DFC], 0x80
|
||||
jz handle_6xE4_return
|
||||
mov ebx, [esp + 0x10] # cmd
|
||||
movzx eax, word [ebx + 2]
|
||||
cmp eax, 0x1000
|
||||
jl handle_6xE4_return
|
||||
cmp eax, 0x1B50
|
||||
jge handle_6xE4_return
|
||||
|
||||
movzx eax, word [ebx + 2]
|
||||
.include GetEnemyEntity-59NJ # auto* ene = get_enemy_entity(cmd->header.entity_id);
|
||||
push eax
|
||||
|
||||
movzx eax, word [ebx + 2]
|
||||
and eax, 0x0FFF
|
||||
imul eax, eax, 0x0C
|
||||
add eax, [0x00AADE38] # eax = state_for_enemy(cmd->header.entity_id)
|
||||
|
||||
cmp dword [ebx + 0x0C], 0
|
||||
jl handle_6xE4_not_proportional
|
||||
mov cx, [ebx + 0x0A] # cmd->max_hp
|
||||
sub cx, [eax + 0x06] # st.total_damage
|
||||
movzx ecx, cx
|
||||
xor edx, edx
|
||||
cmp ecx, edx
|
||||
cmovl ecx, edx
|
||||
push ecx
|
||||
fild st0, dword [esp] # current_hp = static_cast<float>(max<int32_t>(cmd->max_hp - st.total_damage, 0))
|
||||
fld st0, dword [ebx + 0x0C]
|
||||
fmulp st1, st0
|
||||
fistp dword [esp], st0
|
||||
mov ecx, dword [esp] # adjusted_hit_amount = static_cast<int16_t>(current_hp * cmd->factor)
|
||||
add esp, 4
|
||||
xor edx, edx
|
||||
inc edx
|
||||
cmp ecx, edx
|
||||
cmovl ecx, edx
|
||||
mov [ebx + 0x04], cx # cmd->hit_amount = min<int32_t>(1, adjusted_hit_amount)
|
||||
handle_6xE4_not_proportional:
|
||||
|
||||
movzx edx, word [eax + 0x06] # st.total_damage
|
||||
movsx esi, word [ebx + 0x04] # cmd->hit_amount
|
||||
movzx edi, word [ebx + 0x0A] # cmd->max_hp
|
||||
add edx, esi # st.total_damage + cmd->hit_amount
|
||||
cmp edx, edi
|
||||
jl handle_6xE4_damage_less_than_max_hp
|
||||
mov [eax + 0x06], di # st.total_damage = cmd->max_hp;
|
||||
mov edx, [eax]
|
||||
test edx, 0x800
|
||||
jnz handle_6xE4_return_pop_ene
|
||||
or edx, 0x800
|
||||
mov [eax], edx
|
||||
cmp dword [esp], 0
|
||||
je handle_6xE4_return_pop_ene
|
||||
push edx # out_cmd.flags
|
||||
sub esp, 8
|
||||
mov word [esp], 0x030A # out_cmd.header.{subcommand,size}
|
||||
mov si, [ebx + 2]
|
||||
mov [esp + 2], si # out_cmd.header.entity_id
|
||||
and si, 0x0FFF
|
||||
mov [esp + 4], si # out_cmd.entity_index
|
||||
mov [esp + 6], di # out_cmd.total_damage
|
||||
mov ecx, esp
|
||||
mov edx, 0x00801150
|
||||
call edx # send_and_handle_60(&out_cmd);
|
||||
add esp, 0x10
|
||||
jmp handle_6xE4_return
|
||||
|
||||
handle_6xE4_damage_less_than_max_hp:
|
||||
xor edi, edi
|
||||
cmp edx, edx
|
||||
cmovl edx, edi
|
||||
mov [eax + 0x06], dx # st.total_damage = std::max<int16_t>(st.total_damage + cmd->hit_amount, 0);
|
||||
|
||||
mov edx, eax # edx = ene_st
|
||||
mov eax, [esp] # eax = ene
|
||||
test eax, eax
|
||||
jz handle_6xE4_return_pop_ene
|
||||
mov ecx, eax
|
||||
push edx
|
||||
mov edx, [ecx]
|
||||
call [edx + 0x148] # ene->vtable[0x52](ene, &st);
|
||||
|
||||
handle_6xE4_return_pop_ene:
|
||||
add esp, 4
|
||||
handle_6xE4_return:
|
||||
pop edi
|
||||
pop esi
|
||||
pop ebx
|
||||
ret
|
||||
|
||||
handle_6xE4_end:
|
||||
call write_address_of_code
|
||||
|
||||
|
||||
|
||||
# Write TObjectV00b421c0::incr_hp_with_sync
|
||||
push 5
|
||||
push 0x00775224 # TObjectV00b421c0::v18_accept_hit (presumably Resta) - this is add_hp, not subtract_hp!
|
||||
push 5
|
||||
push 0x00778063 # TObjectV00b421c0::subtract_hp_if_not_in_state_2
|
||||
push 5
|
||||
push 0x00777AB2 # TObjectV00b421c0::v19_handle_hit_special_effects
|
||||
push 5
|
||||
push 0x00777B2B # TObjectV00b421c0::v19_handle_hit_special_effects
|
||||
push 5
|
||||
push 0x00777BFC # TObjectV00b421c0::v19_handle_hit_special_effects
|
||||
push 5
|
||||
push 0x00777C75 # TObjectV00b421c0::v19_handle_hit_special_effects
|
||||
push 5
|
||||
push 0x00776D2D # TObjectV00b421c0::v19_handle_hit_special_effects
|
||||
push 5
|
||||
push 0x007769C2 # TObjectV00b421c0::v19_handle_hit_special_effects
|
||||
push 5
|
||||
push 0x0077683C # TObjectV00b421c0::v19_handle_hit_special_effects
|
||||
push 5
|
||||
push 0x00776502 # TObjectV00b421c0::v19_handle_hit_special_effects (Devil's/Demon's)
|
||||
push 5
|
||||
push 0x00775B57 # TObjectV00b421c0::v18_accept_hit
|
||||
push 5
|
||||
push 0x00775A23 # TObjectV00b421c0::v18_accept_hit
|
||||
push 5
|
||||
push 0x007757F0 # TObjectV00b421c0::v18_accept_hit
|
||||
push 5
|
||||
push 0x00775606 # TObjectV00b421c0::v18_accept_hit
|
||||
push 5
|
||||
push 0x007754BC # TObjectV00b421c0::v18_accept_hit
|
||||
push 5
|
||||
push 0x00774E3D # TObjectV00b421c0::v18_accept_hit
|
||||
push 5
|
||||
push 0x00774CD6 # TObjectV00b421c0::v18_accept_hit
|
||||
push 5
|
||||
push 0x00774713 # TObjectV00b421c0::v17
|
||||
push 18
|
||||
call +4
|
||||
.deltaof on_add_or_subtract_hp_start, on_add_or_subtract_hp_end
|
||||
pop eax
|
||||
push dword [eax]
|
||||
call on_add_or_subtract_hp_end
|
||||
|
||||
on_add_or_subtract_hp_start: # (TObjectV00b421c0* this @ ecx, int16_t amount @ [esp + 4]) -> bool @ eax
|
||||
test byte [0x00AA8DFC], 0x80
|
||||
jz on_add_or_subtract_hp_skip_send
|
||||
movzx eax, word [ecx + 0x1C] # ene->entity_id
|
||||
cmp eax, 0x1000
|
||||
jl on_add_or_subtract_hp_skip_send
|
||||
cmp eax, 0x1B50
|
||||
jge on_add_or_subtract_hp_skip_send
|
||||
|
||||
and eax, 0x0FFF
|
||||
imul eax, eax, 0x0C
|
||||
add eax, [0x00AADE38] # eax = state_for_enemy(cmd->header.entity_id)
|
||||
|
||||
sub esp, 0x10
|
||||
mov word [esp], 0x04E4
|
||||
mov dx, [ecx + 0x1C]
|
||||
mov [esp + 0x02], dx # cmd.entity_id
|
||||
mov dx, [esp + 0x14]
|
||||
cmp dword [esp + 0x10], 0x00775229 # Check if callsite is add_hp
|
||||
jne on_add_or_subtract_hp_skip_negate_amount
|
||||
neg dx
|
||||
on_add_or_subtract_hp_skip_negate_amount:
|
||||
mov [esp + 0x04], dx # cmd.hit_amount
|
||||
mov dx, [eax + 6]
|
||||
mov [esp + 0x06], dx # cmd.total_damage_before_hit
|
||||
mov dx, [ecx + 0x0334]
|
||||
mov [esp + 0x08], dx # cmd.current_hp
|
||||
mov dx, [ecx + 0x02BC]
|
||||
mov [esp + 0x0A], dx # cmd.max_hp
|
||||
mov dword [esp + 0x0C], 0xBF800000 # cmd.factor
|
||||
|
||||
cmp dword [esp + 0x10], 0x00776507 # Check if callsite is Devil's/Demon's
|
||||
jne on_add_or_subtract_hp_not_proportional
|
||||
# esp is 0x18 down from where it is in caller's context
|
||||
mov edx, 100
|
||||
sub edx, [esp + 0x24] # edx = (100 - special_amount)
|
||||
push edx
|
||||
fild st0, dword [esp] # current_hp_factor = static_cast<float>(100 - special_amount)
|
||||
fmul st0, dword [esp + 0x54] # *= weapon_reduction_factor
|
||||
mov dword [esp], 0x42C80000 # 100.0f
|
||||
fdiv st0, dword [esp]
|
||||
add esp, 4
|
||||
fstp dword [esp + 0x0C], st0 # cmd.factor = ((100 - special_amount) * weapon_reduction_factor) / 100
|
||||
on_add_or_subtract_hp_not_proportional:
|
||||
|
||||
mov edx, esp
|
||||
push ecx
|
||||
push 0x10
|
||||
push edx
|
||||
mov ecx, [0x00AA8E04]
|
||||
mov edx, 0x007D4CBC
|
||||
call edx # send_60(root_protocol, &cmd, sizeof(cmd));
|
||||
pop ecx
|
||||
add esp, 0x10
|
||||
|
||||
on_add_or_subtract_hp_skip_send:
|
||||
mov eax, 0x007781F0 # subtract_hp
|
||||
mov edx, 0x007781B0 # add_hp
|
||||
cmp dword [esp], 0x00775229 # Check if callsite is add_hp
|
||||
cmove eax, edx
|
||||
jmp eax
|
||||
|
||||
on_add_or_subtract_hp_end:
|
||||
call write_call_to_code_multi
|
||||
|
||||
|
||||
|
||||
push 5
|
||||
push 0x0078864B
|
||||
push 1
|
||||
call +4
|
||||
.deltaof on_6x0A_patch_start, on_6x0A_patch_end
|
||||
pop eax
|
||||
push dword [eax]
|
||||
call on_6x0A_patch_end
|
||||
|
||||
on_6x0A_patch_start: # (TObjectV00b421c0* this @ ecx, int16_t amount @ [esp + 4]) -> bool @ eax
|
||||
test byte [0x00AA8DFC], 0x80
|
||||
jz on_6x0A_patch_skip_write
|
||||
mov [esp + 0x0A], cx
|
||||
on_6x0A_patch_skip_write:
|
||||
ret
|
||||
|
||||
on_6x0A_patch_end:
|
||||
call write_call_to_code_multi
|
||||
|
||||
|
||||
|
||||
ret
|
||||
+14
-12
@@ -1,45 +1,47 @@
|
||||
.meta name="Enemy HP bars"
|
||||
.meta description="Shows HP bars in\nenemy info windows"
|
||||
|
||||
.versions 59NJ 59NL
|
||||
|
||||
entry_ptr:
|
||||
reloc0:
|
||||
.offsetof start
|
||||
start:
|
||||
.include WriteCodeBlocksBB
|
||||
.data 0x007318DD
|
||||
.data <VERS 0x0073197D 0x007318DD>
|
||||
.data 6
|
||||
.binary 81E2FDFFFFFF
|
||||
.data 0x00731F2F
|
||||
.data <VERS 0x00731FCF 0x00731F2F>
|
||||
.data 1
|
||||
.binary FA
|
||||
.data 0x009F2DA4
|
||||
.data <VERS 0x009F0DA4 0x009F2DA4>
|
||||
.data 4
|
||||
.data 0x42480000
|
||||
.data 0x009F2DAC
|
||||
.data <VERS 0x009F0DAC 0x009F2DAC>
|
||||
.data 4
|
||||
.data 0x41C00000
|
||||
.data 0x009F2DD4
|
||||
.data <VERS 0x009F0DD4 0x009F2DD4>
|
||||
.data 4
|
||||
.data 0x42480000
|
||||
.data 0x009F2DDC
|
||||
.data <VERS 0x009F0DDC 0x009F2DDC>
|
||||
.data 4
|
||||
.data 0x41C00000
|
||||
.data 0x009F2E04
|
||||
.data <VERS 0x009F0E04 0x009F2E04>
|
||||
.data 4
|
||||
.data 0x42480000
|
||||
.data 0x009F2E0C
|
||||
.data <VERS 0x009F0E0C 0x009F2E0C>
|
||||
.data 4
|
||||
.data 0x41C00000
|
||||
.data 0x009F2E34
|
||||
.data <VERS 0x009F0E34 0x009F2E34>
|
||||
.data 4
|
||||
.data 0x42480000
|
||||
.data 0x009F2E3C
|
||||
.data <VERS 0x009F0E3C 0x009F2E3C>
|
||||
.data 4
|
||||
.data 0x41C00000
|
||||
.data 0x009F2E64
|
||||
.data <VERS 0x009F0E64 0x009F2E64>
|
||||
.data 4
|
||||
.data 0x42200000
|
||||
.data 0x009F2E80
|
||||
.data <VERS 0x009F0E80 0x009F2E80>
|
||||
.data 4
|
||||
.data 0xFF00FF15
|
||||
.data 0x00000000
|
||||
@@ -1,17 +0,0 @@
|
||||
# This function implements $exit in a game when no quest is loaded.
|
||||
|
||||
.meta name="Exit anywhere"
|
||||
.meta description=""
|
||||
.meta hide_from_patches_menu
|
||||
|
||||
entry_ptr:
|
||||
reloc0:
|
||||
.offsetof start
|
||||
|
||||
start:
|
||||
xor eax, eax
|
||||
mov [0x00A95624], eax # is_in_quest = false
|
||||
mov [0x00A955E0], eax # dat_source_type = NONE
|
||||
inc eax
|
||||
mov [0x00AAE6D4], ax # should_leave_game = true
|
||||
ret
|
||||
@@ -0,0 +1,19 @@
|
||||
# This function implements $exit in a game when no quest is loaded.
|
||||
|
||||
.meta name="Exit anywhere"
|
||||
.meta description=""
|
||||
.meta hide_from_patches_menu
|
||||
|
||||
.versions 59NJ 59NL
|
||||
|
||||
entry_ptr:
|
||||
reloc0:
|
||||
.offsetof start
|
||||
|
||||
start:
|
||||
xor eax, eax
|
||||
mov [<VERS 0x00A931A4 0x00A95624>], eax # is_in_quest = false
|
||||
mov [<VERS 0x00A93160 0x00A955E0>], eax # dat_source_type = NONE
|
||||
inc eax
|
||||
mov [<VERS 0x00AAC254 0x00AAE6D4>], ax # should_leave_game = true
|
||||
ret
|
||||
+4
-2
@@ -1,19 +1,21 @@
|
||||
.meta name="Fast tekker"
|
||||
.meta description="Skips wind-up sound\nat tekker window"
|
||||
|
||||
.versions 59NJ 59NL
|
||||
|
||||
entry_ptr:
|
||||
reloc0:
|
||||
.offsetof start
|
||||
start:
|
||||
.include WriteCodeBlocksBB
|
||||
|
||||
.data 0x006DA113
|
||||
.data <VERS 0x006DA14B 0x006DA113>
|
||||
.deltaof patch1_start, patch1_end
|
||||
patch1_start:
|
||||
mov dword [edi + 0x14C], 1
|
||||
patch1_end:
|
||||
|
||||
.data 0x006DA130
|
||||
.data <VERS 0x006DA168 0x006DA130>
|
||||
.deltaof patch2_start, patch2_end
|
||||
patch2_start:
|
||||
nop
|
||||
@@ -0,0 +1,34 @@
|
||||
.meta name="MAG alert"
|
||||
.meta description="Plays a sound when\nyour MAG is hungry"
|
||||
|
||||
entry_ptr:
|
||||
reloc0:
|
||||
.offsetof start
|
||||
start:
|
||||
pop ecx
|
||||
push 6
|
||||
push 0x005D91BE
|
||||
call get_code_size
|
||||
.deltaof patch_code, patch_code_end
|
||||
get_code_size:
|
||||
pop eax
|
||||
push dword [eax]
|
||||
call patch_code_end
|
||||
patch_code: # [eax] (TItemMag* this @ ecx) -> void
|
||||
mov dword [ecx + 0x01B8], eax
|
||||
mov eax, [ecx + 0x00F8]
|
||||
movzx eax, word [eax + 0x001C] # eax = this->owner_player->entity_id
|
||||
cmp [0x00A9A074], eax
|
||||
jne patch_code_skip_sound
|
||||
push 0
|
||||
push 0
|
||||
push 0
|
||||
push 0xAC
|
||||
mov eax, 0x00815020
|
||||
call eax
|
||||
add esp, 0x10
|
||||
patch_code_skip_sound:
|
||||
ret
|
||||
patch_code_end:
|
||||
push ecx
|
||||
.include WriteCallToCode-59NJ
|
||||
@@ -0,0 +1,43 @@
|
||||
# Original patch by Soly, in Blue Burst Patch Project
|
||||
# https://github.com/Solybum/Blue-Burst-Patch-Project
|
||||
|
||||
.meta name="No rare selling"
|
||||
.meta description="Stops you from accidentally\nselling rares to vendors"
|
||||
|
||||
entry_ptr:
|
||||
reloc0:
|
||||
.offsetof start
|
||||
start:
|
||||
# This works by setting the item price to zero if it's rare, which causes
|
||||
# the game to prevent you from selling the item. For armors and weapons, this
|
||||
# is easy because there are easily-patchable opcodes within branches that
|
||||
# return a constant price for rare items.
|
||||
xor eax, eax
|
||||
mov [0x005D258F], eax # Rare armors
|
||||
mov [0x005D26D1], eax # Unidentified weapons
|
||||
mov [0x005D26E6], eax # Rare weapons
|
||||
|
||||
# For tools, it's harder to implement this, because the price comes from the
|
||||
# ItemPMT tools table and there is no branch for rares. Still, we can add a
|
||||
# branch to a stub to handle tools.
|
||||
pop ecx
|
||||
push 5
|
||||
push 0x005D2508
|
||||
call get_code_size
|
||||
.deltaof patch_code, patch_code_end
|
||||
get_code_size:
|
||||
pop eax
|
||||
push dword [eax]
|
||||
call patch_code_end
|
||||
patch_code:
|
||||
# TODO: It'd be nice to have something like WriteJumpToAndFromCode, since
|
||||
# this hook is supposed to return to a different place than where it was
|
||||
# called, hence this mov [esp].
|
||||
mov dword [esp], 0x005D2556
|
||||
xor edi, edi
|
||||
test byte [eax + 0x14], 0x80 # flags & 0x80 = is rare
|
||||
cmovz edi, [eax + 0x10] # Use price from table if not rare
|
||||
ret
|
||||
patch_code_end:
|
||||
push ecx
|
||||
.include WriteCallToCode-59NJ
|
||||
@@ -0,0 +1,230 @@
|
||||
# Original patch by Soly, in Blue Burst Patch Project
|
||||
# https://github.com/Solybum/Blue-Burst-Patch-Project
|
||||
|
||||
.meta name="Palette"
|
||||
.meta description="Enables the alternate action\npalette for number keys"
|
||||
|
||||
entry_ptr:
|
||||
reloc0:
|
||||
.offsetof start
|
||||
|
||||
write_call_func:
|
||||
.include WriteCallToCode-59NJ
|
||||
|
||||
start:
|
||||
mov al, 0xEB
|
||||
mov [0x0068A7A5], al # SecondaryPaletteAttack1
|
||||
xor al, al
|
||||
mov [0x006A11B7], al # SecondaryPaletteAttack2
|
||||
mov [0x006A0CB7], al # SecondaryPaletteAttack3
|
||||
|
||||
call patch_func_1 # GetCurrentPalette
|
||||
call patch_func_2 # CheckHotkey1_1
|
||||
call patch_func_3 # CheckHotkey1_2
|
||||
call patch_func_4 # CheckHotkey2_1
|
||||
call patch_func_5 # CheckHotkey2_2
|
||||
call patch_func_6 # CheckHotkey3_1
|
||||
call patch_func_7 # CheckHotkey3_2
|
||||
jmp write_code_blocks # UnsetHotkey1, UnsetHotkey2, SetHotkey
|
||||
|
||||
# GetCurrentPalette
|
||||
patch_func_1:
|
||||
pop ecx
|
||||
push 8
|
||||
push 0x00748990
|
||||
call get_code_size1
|
||||
.deltaof patch_code1, patch_code_end1
|
||||
get_code_size1:
|
||||
pop eax
|
||||
push dword [eax]
|
||||
call patch_code_end1
|
||||
patch_code1:
|
||||
mov edx, [ebp - 0x14]
|
||||
mov edx, [edx + 0x2C]
|
||||
movzx edx, byte [edx + 0x62]
|
||||
test edx, edx
|
||||
setnz byte [0x00748B1B]
|
||||
mov edx, edi
|
||||
and edx, 0xFF
|
||||
ret
|
||||
patch_code_end1:
|
||||
push ecx
|
||||
jmp write_call_func
|
||||
|
||||
# CheckHotkey1_1
|
||||
patch_func_2:
|
||||
pop ecx
|
||||
push 5
|
||||
push 0x007489DE
|
||||
call get_code_size2
|
||||
.deltaof patch_code2, patch_code_end2
|
||||
get_code_size2:
|
||||
pop eax
|
||||
push dword [eax]
|
||||
call patch_code_end2
|
||||
patch_code2:
|
||||
cmp byte [0x00748B1B], 0
|
||||
jnz +0x06
|
||||
movzx edx, byte [eax + esi * 4 + 0x04] # main palette
|
||||
ret
|
||||
movzx edx, byte [eax + esi * 4 + 0x3C] # alt palette
|
||||
ret
|
||||
patch_code_end2:
|
||||
push ecx
|
||||
jmp write_call_func
|
||||
|
||||
# CheckHotkey1_2
|
||||
patch_func_3:
|
||||
pop ecx
|
||||
push 5
|
||||
push 0x007489ED
|
||||
call get_code_size3
|
||||
.deltaof patch_code3, patch_code_end3
|
||||
get_code_size3:
|
||||
pop eax
|
||||
push dword [eax]
|
||||
call patch_code_end3
|
||||
patch_code3:
|
||||
cmp byte [0x00748B1B], 0
|
||||
jnz +0x06
|
||||
movzx ecx, byte [eax + ecx * 2 + 0x05] # main palette
|
||||
ret
|
||||
movzx ecx, byte [eax + ecx * 2 + 0x3D] # alt palette
|
||||
ret
|
||||
patch_code_end3:
|
||||
push ecx
|
||||
jmp write_call_func
|
||||
|
||||
# CheckHotkey2_1
|
||||
patch_func_4:
|
||||
pop ecx
|
||||
push 5
|
||||
push 0x00748A88
|
||||
call get_code_size4
|
||||
.deltaof patch_code4, patch_code_end4
|
||||
get_code_size4:
|
||||
pop eax
|
||||
push dword [eax]
|
||||
call patch_code_end4
|
||||
patch_code4:
|
||||
cmp byte [0x00748B1B], 0
|
||||
jnz +0x06
|
||||
movzx edx, byte [edx + ebx * 4 + 0x04] # main palette
|
||||
ret
|
||||
movzx edx, byte [edx + ebx * 4 + 0x3C] # alt palette
|
||||
ret
|
||||
patch_code_end4:
|
||||
push ecx
|
||||
jmp write_call_func
|
||||
|
||||
# CheckHotkey2_2
|
||||
patch_func_5:
|
||||
pop ecx
|
||||
push 5
|
||||
push 0x00748A97
|
||||
call get_code_size5
|
||||
.deltaof patch_code5, patch_code_end5
|
||||
get_code_size5:
|
||||
pop eax
|
||||
push dword [eax]
|
||||
call patch_code_end5
|
||||
patch_code5:
|
||||
cmp byte [0x00748B1B], 0
|
||||
jnz +0x06
|
||||
movzx ecx, byte [edx + eax * 2 + 0x05] # main palette
|
||||
ret
|
||||
movzx ecx, byte [edx + eax * 2 + 0x3D] # alt palette
|
||||
ret
|
||||
patch_code_end5:
|
||||
push ecx
|
||||
jmp write_call_func
|
||||
|
||||
# CheckHotkey3_1
|
||||
patch_func_6:
|
||||
pop ecx
|
||||
push 5
|
||||
push 0x007103D3
|
||||
call get_code_size6
|
||||
.deltaof patch_code6, patch_code_end6
|
||||
get_code_size6:
|
||||
pop eax
|
||||
push dword [eax]
|
||||
call patch_code_end6
|
||||
patch_code6:
|
||||
cmp byte [0x00748B1B], 0
|
||||
jnz +0x06
|
||||
movzx ecx, byte [eax + edx * 4 + 0x04] # main palette
|
||||
ret
|
||||
movzx ecx, byte [eax + edx * 4 + 0x3C] # alt palette
|
||||
ret
|
||||
patch_code_end6:
|
||||
push ecx
|
||||
jmp write_call_func
|
||||
|
||||
# CheckHotkey3_2
|
||||
patch_func_7:
|
||||
pop ecx
|
||||
push 5
|
||||
push 0x007103DC
|
||||
call get_code_size7
|
||||
.deltaof patch_code7, patch_code_end7
|
||||
get_code_size7:
|
||||
pop eax
|
||||
push dword [eax]
|
||||
call patch_code_end7
|
||||
patch_code7:
|
||||
cmp byte [0x00748B1B], 0
|
||||
jnz +0x06
|
||||
movzx ecx, byte [eax + edx * 4 + 0x05] # main palette
|
||||
ret
|
||||
movzx ecx, byte [eax + edx * 4 + 0x3D] # alt palette
|
||||
ret
|
||||
patch_code_end7:
|
||||
push ecx
|
||||
jmp write_call_func
|
||||
|
||||
write_code_blocks:
|
||||
.include WriteCodeBlocksBB
|
||||
|
||||
.data 0x00748A05
|
||||
.deltaof code_block1_start, code_block1_end
|
||||
|
||||
# UnsetHotkey1
|
||||
code_block1_start:
|
||||
push dword [0x00748B1B]
|
||||
push eax
|
||||
mov eax, 0x0068CE4C # SetPaletteHotkey
|
||||
call eax
|
||||
.binary 909090909090909090
|
||||
code_block1_end:
|
||||
.data 0x00748AAB
|
||||
.deltaof code_block2_start, code_block2_end
|
||||
|
||||
# UnsetHotkey2
|
||||
code_block2_start:
|
||||
push dword [0x00748B1B]
|
||||
push eax
|
||||
mov eax, 0x0068CE4C # SetPaletteHotkey
|
||||
call eax
|
||||
.binary 909090909090909090
|
||||
code_block2_end:
|
||||
.data 0x00748B0A
|
||||
.deltaof code_block3_start, code_block3_end
|
||||
|
||||
# SetHotkey
|
||||
code_block3_start:
|
||||
mov eax, [ebp - 0x24]
|
||||
mov ecx, [ebp - 0x28]
|
||||
movzx ebx, word [eax]
|
||||
movzx edx, word [eax + 0x02]
|
||||
push edx
|
||||
push ebx
|
||||
push esi
|
||||
.binary 6800000000 # tmpCurrentPalette = 0x00748B1B
|
||||
push 0
|
||||
mov eax, 0x0068CE4C # SetPaletteHotkey
|
||||
call eax
|
||||
.binary 90909090909090909090909090909090
|
||||
code_block3_end:
|
||||
.data 0x00000000
|
||||
.data 0x00000000
|
||||
@@ -0,0 +1,20 @@
|
||||
.meta name="PSO Peeps Dragon HP"
|
||||
.meta description="Sets Normal Dragon HP\nto 1800 for V2 crossplay"
|
||||
|
||||
entry_ptr:
|
||||
reloc0:
|
||||
.offsetof start
|
||||
|
||||
start:
|
||||
.include WriteCodeBlocksGC
|
||||
|
||||
# GC Plus USA / 3OE1
|
||||
# BattleParamEntry_on.dat loaded Normal Dragon row at 0x811ABA48
|
||||
# HP field is row + 0x06 = 0x811ABA4E
|
||||
# 2500 = 09 C4; 1800 = 07 08
|
||||
.data 0x811ABA4E
|
||||
.data 2
|
||||
.binary 0708
|
||||
|
||||
.data 0
|
||||
.data 0
|
||||
@@ -0,0 +1,834 @@
|
||||
.meta name="PSO Peeps EP1 10x EXP"
|
||||
.meta description="Sets EP1 enemy EXP\nto 10x for GC crossplay"
|
||||
|
||||
entry_ptr:
|
||||
reloc0:
|
||||
.offsetof start
|
||||
|
||||
start:
|
||||
.include WriteCodeBlocksGC
|
||||
|
||||
# PSO Peeps GC Plus USA / 3OE1
|
||||
# Source table: BattleParamEntry_on.dat
|
||||
# Active online battle-param table loaded at 0x811AB7C0
|
||||
# EXP field offset within each 0x24-byte row is +0x1C
|
||||
# Generated from clean BattleParamEntry_on.dat; multiplier=10x
|
||||
|
||||
.data 0x811AB7DC
|
||||
.data 4
|
||||
.binary 0000000a
|
||||
|
||||
.data 0x811AB800
|
||||
.data 4
|
||||
.binary 0000003c
|
||||
|
||||
.data 0x811AB824
|
||||
.data 4
|
||||
.binary 00000032
|
||||
|
||||
.data 0x811AB848
|
||||
.data 4
|
||||
.binary 00000046
|
||||
|
||||
.data 0x811AB86C
|
||||
.data 4
|
||||
.binary 00000064
|
||||
|
||||
.data 0x811AB890
|
||||
.data 4
|
||||
.binary 000005dc
|
||||
|
||||
.data 0x811AB8B4
|
||||
.data 4
|
||||
.binary 000000fa
|
||||
|
||||
.data 0x811AB8D8
|
||||
.data 4
|
||||
.binary 000000a0
|
||||
|
||||
.data 0x811AB8FC
|
||||
.data 4
|
||||
.binary 000000a0
|
||||
|
||||
.data 0x811AB920
|
||||
.data 4
|
||||
.binary 000000aa
|
||||
|
||||
.data 0x811AB944
|
||||
.data 4
|
||||
.binary 00000122
|
||||
|
||||
.data 0x811AB968
|
||||
.data 4
|
||||
.binary 00000028
|
||||
|
||||
.data 0x811AB98C
|
||||
.data 4
|
||||
.binary 00000028
|
||||
|
||||
.data 0x811AB9B0
|
||||
.data 4
|
||||
.binary 0000015e
|
||||
|
||||
.data 0x811AB9D4
|
||||
.data 4
|
||||
.binary 0000015e
|
||||
|
||||
.data 0x811AB9F8
|
||||
.data 4
|
||||
.binary 000022c4
|
||||
|
||||
.data 0x811ABA1C
|
||||
.data 4
|
||||
.binary 00000064
|
||||
|
||||
.data 0x811ABA40
|
||||
.data 4
|
||||
.binary 00000064
|
||||
|
||||
.data 0x811ABA64
|
||||
.data 4
|
||||
.binary 00000fa0
|
||||
|
||||
.data 0x811ABA88
|
||||
.data 4
|
||||
.binary 00000118
|
||||
|
||||
.data 0x811ABB3C
|
||||
.data 4
|
||||
.binary 00000028
|
||||
|
||||
.data 0x811ABB60
|
||||
.data 4
|
||||
.binary 000003e8
|
||||
|
||||
.data 0x811ABB84
|
||||
.data 4
|
||||
.binary 00000096
|
||||
|
||||
.data 0x811ABBA8
|
||||
.data 4
|
||||
.binary 0000001e
|
||||
|
||||
.data 0x811ABBCC
|
||||
.data 4
|
||||
.binary 000000b4
|
||||
|
||||
.data 0x811ABBF0
|
||||
.data 4
|
||||
.binary 000000dc
|
||||
|
||||
.data 0x811ABC14
|
||||
.data 4
|
||||
.binary 000000c8
|
||||
|
||||
.data 0x811ABC38
|
||||
.data 4
|
||||
.binary 0000010e
|
||||
|
||||
.data 0x811ABC5C
|
||||
.data 4
|
||||
.binary 0000003c
|
||||
|
||||
.data 0x811ABD10
|
||||
.data 4
|
||||
.binary 000030d4
|
||||
|
||||
.data 0x811ABE9C
|
||||
.data 4
|
||||
.binary 00000064
|
||||
|
||||
.data 0x811ABEC0
|
||||
.data 4
|
||||
.binary 0000003c
|
||||
|
||||
.data 0x811ABEE4
|
||||
.data 4
|
||||
.binary 00000028
|
||||
|
||||
.data 0x811ABF08
|
||||
.data 4
|
||||
.binary 00000028
|
||||
|
||||
.data 0x811ABF2C
|
||||
.data 4
|
||||
.binary 000005dc
|
||||
|
||||
.data 0x811ABF50
|
||||
.data 4
|
||||
.binary 00000032
|
||||
|
||||
.data 0x811ABF98
|
||||
.data 4
|
||||
.binary 00007530
|
||||
|
||||
.data 0x811ABFE0
|
||||
.data 4
|
||||
.binary 00000032
|
||||
|
||||
.data 0x811AC220
|
||||
.data 4
|
||||
.binary 00000082
|
||||
|
||||
.data 0x811AC244
|
||||
.data 4
|
||||
.binary 000003e8
|
||||
|
||||
.data 0x811AC268
|
||||
.data 4
|
||||
.binary 00000032
|
||||
|
||||
.data 0x811AC28C
|
||||
.data 4
|
||||
.binary 0000003c
|
||||
|
||||
.data 0x811AC2B0
|
||||
.data 4
|
||||
.binary 00000046
|
||||
|
||||
.data 0x811AC2D4
|
||||
.data 4
|
||||
.binary 000000c8
|
||||
|
||||
.data 0x811AC2F8
|
||||
.data 4
|
||||
.binary 00000064
|
||||
|
||||
.data 0x811AC31C
|
||||
.data 4
|
||||
.binary 00000078
|
||||
|
||||
.data 0x811AC340
|
||||
.data 4
|
||||
.binary 0000008c
|
||||
|
||||
.data 0x811AC364
|
||||
.data 4
|
||||
.binary 0000012c
|
||||
|
||||
.data 0x811AC388
|
||||
.data 4
|
||||
.binary 000000dc
|
||||
|
||||
.data 0x811AC3AC
|
||||
.data 4
|
||||
.binary 000000f0
|
||||
|
||||
.data 0x811AC3D0
|
||||
.data 4
|
||||
.binary 00000104
|
||||
|
||||
.data 0x811AC55C
|
||||
.data 4
|
||||
.binary 00000168
|
||||
|
||||
.data 0x811AC580
|
||||
.data 4
|
||||
.binary 000001ae
|
||||
|
||||
.data 0x811AC5A4
|
||||
.data 4
|
||||
.binary 000001a4
|
||||
|
||||
.data 0x811AC5C8
|
||||
.data 4
|
||||
.binary 000001c2
|
||||
|
||||
.data 0x811AC5EC
|
||||
.data 4
|
||||
.binary 000001ea
|
||||
|
||||
.data 0x811AC610
|
||||
.data 4
|
||||
.binary 00000a1e
|
||||
|
||||
.data 0x811AC634
|
||||
.data 4
|
||||
.binary 000002bc
|
||||
|
||||
.data 0x811AC658
|
||||
.data 4
|
||||
.binary 00000244
|
||||
|
||||
.data 0x811AC67C
|
||||
.data 4
|
||||
.binary 00000244
|
||||
|
||||
.data 0x811AC6A0
|
||||
.data 4
|
||||
.binary 00000258
|
||||
|
||||
.data 0x811AC6C4
|
||||
.data 4
|
||||
.binary 0000030c
|
||||
|
||||
.data 0x811AC6E8
|
||||
.data 4
|
||||
.binary 00000190
|
||||
|
||||
.data 0x811AC70C
|
||||
.data 4
|
||||
.binary 00000190
|
||||
|
||||
.data 0x811AC730
|
||||
.data 4
|
||||
.binary 000003ca
|
||||
|
||||
.data 0x811AC754
|
||||
.data 4
|
||||
.binary 00000348
|
||||
|
||||
.data 0x811AC778
|
||||
.data 4
|
||||
.binary 00007d00
|
||||
|
||||
.data 0x811AC79C
|
||||
.data 4
|
||||
.binary 000000fa
|
||||
|
||||
.data 0x811AC7C0
|
||||
.data 4
|
||||
.binary 00000028
|
||||
|
||||
.data 0x811AC7E4
|
||||
.data 4
|
||||
.binary 00005dc0
|
||||
|
||||
.data 0x811AC808
|
||||
.data 4
|
||||
.binary 000002ee
|
||||
|
||||
.data 0x811AC8BC
|
||||
.data 4
|
||||
.binary 00000190
|
||||
|
||||
.data 0x811AC8E0
|
||||
.data 4
|
||||
.binary 00000730
|
||||
|
||||
.data 0x811AC904
|
||||
.data 4
|
||||
.binary 0000023a
|
||||
|
||||
.data 0x811AC928
|
||||
.data 4
|
||||
.binary 0000006e
|
||||
|
||||
.data 0x811AC94C
|
||||
.data 4
|
||||
.binary 00000262
|
||||
|
||||
.data 0x811AC970
|
||||
.data 4
|
||||
.binary 0000029e
|
||||
|
||||
.data 0x811AC994
|
||||
.data 4
|
||||
.binary 00000280
|
||||
|
||||
.data 0x811AC9B8
|
||||
.data 4
|
||||
.binary 00000302
|
||||
|
||||
.data 0x811AC9DC
|
||||
.data 4
|
||||
.binary 000001ae
|
||||
|
||||
.data 0x811ACA90
|
||||
.data 4
|
||||
.binary 00009470
|
||||
|
||||
.data 0x811ACC1C
|
||||
.data 4
|
||||
.binary 000001ea
|
||||
|
||||
.data 0x811ACC40
|
||||
.data 4
|
||||
.binary 00000104
|
||||
|
||||
.data 0x811ACC64
|
||||
.data 4
|
||||
.binary 000000e6
|
||||
|
||||
.data 0x811ACC88
|
||||
.data 4
|
||||
.binary 000000e6
|
||||
|
||||
.data 0x811ACCAC
|
||||
.data 4
|
||||
.binary 00000a1e
|
||||
|
||||
.data 0x811ACCD0
|
||||
.data 4
|
||||
.binary 00000032
|
||||
|
||||
.data 0x811ACD3C
|
||||
.data 4
|
||||
.binary 00013880
|
||||
|
||||
.data 0x811ACD60
|
||||
.data 4
|
||||
.binary 00000032
|
||||
|
||||
.data 0x811ACFA0
|
||||
.data 4
|
||||
.binary 00000230
|
||||
|
||||
.data 0x811ACFC4
|
||||
.data 4
|
||||
.binary 00000730
|
||||
|
||||
.data 0x811ACFE8
|
||||
.data 4
|
||||
.binary 000001a4
|
||||
|
||||
.data 0x811AD00C
|
||||
.data 4
|
||||
.binary 000001ae
|
||||
|
||||
.data 0x811AD030
|
||||
.data 4
|
||||
.binary 000001c2
|
||||
|
||||
.data 0x811AD054
|
||||
.data 4
|
||||
.binary 00000280
|
||||
|
||||
.data 0x811AD078
|
||||
.data 4
|
||||
.binary 000001ea
|
||||
|
||||
.data 0x811AD09C
|
||||
.data 4
|
||||
.binary 00000208
|
||||
|
||||
.data 0x811AD0C0
|
||||
.data 4
|
||||
.binary 00000226
|
||||
|
||||
.data 0x811AD0E4
|
||||
.data 4
|
||||
.binary 0000032a
|
||||
|
||||
.data 0x811AD108
|
||||
.data 4
|
||||
.binary 0000029e
|
||||
|
||||
.data 0x811AD12C
|
||||
.data 4
|
||||
.binary 000002bc
|
||||
|
||||
.data 0x811AD150
|
||||
.data 4
|
||||
.binary 000002da
|
||||
|
||||
.data 0x811AD2DC
|
||||
.data 4
|
||||
.binary 00000334
|
||||
|
||||
.data 0x811AD300
|
||||
.data 4
|
||||
.binary 00000398
|
||||
|
||||
.data 0x811AD324
|
||||
.data 4
|
||||
.binary 00000384
|
||||
|
||||
.data 0x811AD348
|
||||
.data 4
|
||||
.binary 000003ac
|
||||
|
||||
.data 0x811AD36C
|
||||
.data 4
|
||||
.binary 000003e8
|
||||
|
||||
.data 0x811AD390
|
||||
.data 4
|
||||
.binary 00000ed8
|
||||
|
||||
.data 0x811AD3B4
|
||||
.data 4
|
||||
.binary 0000055a
|
||||
|
||||
.data 0x811AD3D8
|
||||
.data 4
|
||||
.binary 00000460
|
||||
|
||||
.data 0x811AD3FC
|
||||
.data 4
|
||||
.binary 00000460
|
||||
|
||||
.data 0x811AD420
|
||||
.data 4
|
||||
.binary 00000474
|
||||
|
||||
.data 0x811AD444
|
||||
.data 4
|
||||
.binary 000005c8
|
||||
|
||||
.data 0x811AD468
|
||||
.data 4
|
||||
.binary 00000370
|
||||
|
||||
.data 0x811AD48C
|
||||
.data 4
|
||||
.binary 00000370
|
||||
|
||||
.data 0x811AD4B0
|
||||
.data 4
|
||||
.binary 000005dc
|
||||
|
||||
.data 0x811AD4D4
|
||||
.data 4
|
||||
.binary 000005a0
|
||||
|
||||
.data 0x811AD4F8
|
||||
.data 4
|
||||
.binary 000153d8
|
||||
|
||||
.data 0x811AD51C
|
||||
.data 4
|
||||
.binary 0000012c
|
||||
|
||||
.data 0x811AD540
|
||||
.data 4
|
||||
.binary 00000050
|
||||
|
||||
.data 0x811AD564
|
||||
.data 4
|
||||
.binary 000137b8
|
||||
|
||||
.data 0x811AD588
|
||||
.data 4
|
||||
.binary 00000596
|
||||
|
||||
.data 0x811AD63C
|
||||
.data 4
|
||||
.binary 00000370
|
||||
|
||||
.data 0x811AD660
|
||||
.data 4
|
||||
.binary 00000af0
|
||||
|
||||
.data 0x811AD684
|
||||
.data 4
|
||||
.binary 000004b0
|
||||
|
||||
.data 0x811AD6A8
|
||||
.data 4
|
||||
.binary 000000dc
|
||||
|
||||
.data 0x811AD6CC
|
||||
.data 4
|
||||
.binary 00000488
|
||||
|
||||
.data 0x811AD6F0
|
||||
.data 4
|
||||
.binary 000004d8
|
||||
|
||||
.data 0x811AD714
|
||||
.data 4
|
||||
.binary 000004b0
|
||||
|
||||
.data 0x811AD738
|
||||
.data 4
|
||||
.binary 00000500
|
||||
|
||||
.data 0x811AD75C
|
||||
.data 4
|
||||
.binary 00000398
|
||||
|
||||
.data 0x811AD810
|
||||
.data 4
|
||||
.binary 0001b198
|
||||
|
||||
.data 0x811AD99C
|
||||
.data 4
|
||||
.binary 000003e8
|
||||
|
||||
.data 0x811AD9C0
|
||||
.data 4
|
||||
.binary 00000208
|
||||
|
||||
.data 0x811AD9E4
|
||||
.data 4
|
||||
.binary 000001e0
|
||||
|
||||
.data 0x811ADA08
|
||||
.data 4
|
||||
.binary 000001e0
|
||||
|
||||
.data 0x811ADA2C
|
||||
.data 4
|
||||
.binary 00000ed8
|
||||
|
||||
.data 0x811ADA50
|
||||
.data 4
|
||||
.binary 0000005a
|
||||
|
||||
.data 0x811ADABC
|
||||
.data 4
|
||||
.binary 00027100
|
||||
|
||||
.data 0x811ADAE0
|
||||
.data 4
|
||||
.binary 0000005a
|
||||
|
||||
.data 0x811ADD20
|
||||
.data 4
|
||||
.binary 00000456
|
||||
|
||||
.data 0x811ADD44
|
||||
.data 4
|
||||
.binary 00000af0
|
||||
|
||||
.data 0x811ADD68
|
||||
.data 4
|
||||
.binary 00000384
|
||||
|
||||
.data 0x811ADD8C
|
||||
.data 4
|
||||
.binary 00000398
|
||||
|
||||
.data 0x811ADDB0
|
||||
.data 4
|
||||
.binary 000003ac
|
||||
|
||||
.data 0x811ADDD4
|
||||
.data 4
|
||||
.binary 000004e2
|
||||
|
||||
.data 0x811ADDF8
|
||||
.data 4
|
||||
.binary 000003e8
|
||||
|
||||
.data 0x811ADE1C
|
||||
.data 4
|
||||
.binary 00000410
|
||||
|
||||
.data 0x811ADE40
|
||||
.data 4
|
||||
.binary 00000438
|
||||
|
||||
.data 0x811ADE64
|
||||
.data 4
|
||||
.binary 000005dc
|
||||
|
||||
.data 0x811ADE88
|
||||
.data 4
|
||||
.binary 000004d8
|
||||
|
||||
.data 0x811ADEAC
|
||||
.data 4
|
||||
.binary 00000500
|
||||
|
||||
.data 0x811ADED0
|
||||
.data 4
|
||||
.binary 00000528
|
||||
|
||||
.data 0x811AE05C
|
||||
.data 4
|
||||
.binary 000005aa
|
||||
|
||||
.data 0x811AE080
|
||||
.data 4
|
||||
.binary 00000a8c
|
||||
|
||||
.data 0x811AE0A4
|
||||
.data 4
|
||||
.binary 00000abe
|
||||
|
||||
.data 0x811AE0C8
|
||||
.data 4
|
||||
.binary 00000af0
|
||||
|
||||
.data 0x811AE0EC
|
||||
.data 4
|
||||
.binary 00000b86
|
||||
|
||||
.data 0x811AE110
|
||||
.data 4
|
||||
.binary 00002328
|
||||
|
||||
.data 0x811AE134
|
||||
.data 4
|
||||
.binary 00000dac
|
||||
|
||||
.data 0x811AE158
|
||||
.data 4
|
||||
.binary 00000c80
|
||||
|
||||
.data 0x811AE17C
|
||||
.data 4
|
||||
.binary 00000c80
|
||||
|
||||
.data 0x811AE1A0
|
||||
.data 4
|
||||
.binary 00000cb2
|
||||
|
||||
.data 0x811AE1C4
|
||||
.data 4
|
||||
.binary 00000e2e
|
||||
|
||||
.data 0x811AE1E8
|
||||
.data 4
|
||||
.binary 000003e8
|
||||
|
||||
.data 0x811AE20C
|
||||
.data 4
|
||||
.binary 000003e8
|
||||
|
||||
.data 0x811AE230
|
||||
.data 4
|
||||
.binary 00000f0a
|
||||
|
||||
.data 0x811AE254
|
||||
.data 4
|
||||
.binary 00000e88
|
||||
|
||||
.data 0x811AE278
|
||||
.data 4
|
||||
.binary 0002de60
|
||||
|
||||
.data 0x811AE29C
|
||||
.data 4
|
||||
.binary 00000258
|
||||
|
||||
.data 0x811AE2C0
|
||||
.data 4
|
||||
.binary 000000a0
|
||||
|
||||
.data 0x811AE2E4
|
||||
.data 4
|
||||
.binary 00026d18
|
||||
|
||||
.data 0x811AE308
|
||||
.data 4
|
||||
.binary 00000d70
|
||||
|
||||
.data 0x811AE3BC
|
||||
.data 4
|
||||
.binary 00000a00
|
||||
|
||||
.data 0x811AE3E0
|
||||
.data 4
|
||||
.binary 00001b58
|
||||
|
||||
.data 0x811AE404
|
||||
.data 4
|
||||
.binary 00000c80
|
||||
|
||||
.data 0x811AE428
|
||||
.data 4
|
||||
.binary 0000024e
|
||||
|
||||
.data 0x811AE44C
|
||||
.data 4
|
||||
.binary 00000c8a
|
||||
|
||||
.data 0x811AE470
|
||||
.data 4
|
||||
.binary 00000dde
|
||||
|
||||
.data 0x811AE494
|
||||
.data 4
|
||||
.binary 00000d0c
|
||||
|
||||
.data 0x811AE4B8
|
||||
.data 4
|
||||
.binary 00000d0c
|
||||
|
||||
.data 0x811AE4DC
|
||||
.data 4
|
||||
.binary 00000aaa
|
||||
|
||||
.data 0x811AE590
|
||||
.data 4
|
||||
.binary 000395f8
|
||||
|
||||
.data 0x811AE71C
|
||||
.data 4
|
||||
.binary 00000b54
|
||||
|
||||
.data 0x811AE740
|
||||
.data 4
|
||||
.binary 0000079e
|
||||
|
||||
.data 0x811AE764
|
||||
.data 4
|
||||
.binary 00000708
|
||||
|
||||
.data 0x811AE788
|
||||
.data 4
|
||||
.binary 00000708
|
||||
|
||||
.data 0x811AE7AC
|
||||
.data 4
|
||||
.binary 00002260
|
||||
|
||||
.data 0x811AE7D0
|
||||
.data 4
|
||||
.binary 000000fa
|
||||
|
||||
.data 0x811AE83C
|
||||
.data 4
|
||||
.binary 0007a120
|
||||
|
||||
.data 0x811AE860
|
||||
.data 4
|
||||
.binary 000000c8
|
||||
|
||||
.data 0x811AEAA0
|
||||
.data 4
|
||||
.binary 00000bb8
|
||||
|
||||
.data 0x811AEAC4
|
||||
.data 4
|
||||
.binary 00001af4
|
||||
|
||||
.data 0x811AEAE8
|
||||
.data 4
|
||||
.binary 00000a96
|
||||
|
||||
.data 0x811AEB0C
|
||||
.data 4
|
||||
.binary 00000ac8
|
||||
|
||||
.data 0x811AEB30
|
||||
.data 4
|
||||
.binary 00000a78
|
||||
|
||||
.data 0x811AEB54
|
||||
.data 4
|
||||
.binary 00000d02
|
||||
|
||||
.data 0x811AEB78
|
||||
.data 4
|
||||
.binary 00000b86
|
||||
|
||||
.data 0x811AEB9C
|
||||
.data 4
|
||||
.binary 00000bd6
|
||||
|
||||
.data 0x811AEBC0
|
||||
.data 4
|
||||
.binary 00000c6c
|
||||
|
||||
.data 0x811AEBE4
|
||||
.data 4
|
||||
.binary 00000ea6
|
||||
|
||||
.data 0x811AEC08
|
||||
.data 4
|
||||
.binary 00000d3e
|
||||
|
||||
.data 0x811AEC2C
|
||||
.data 4
|
||||
.binary 00000d7a
|
||||
|
||||
.data 0x811AEC50
|
||||
.data 4
|
||||
.binary 00000de8
|
||||
|
||||
.data 0
|
||||
.data 0
|
||||
@@ -0,0 +1,834 @@
|
||||
.meta name="PSO Peeps EP1 5x EXP"
|
||||
.meta description="Sets EP1 enemy EXP\nto 5x for GC crossplay"
|
||||
|
||||
entry_ptr:
|
||||
reloc0:
|
||||
.offsetof start
|
||||
|
||||
start:
|
||||
.include WriteCodeBlocksGC
|
||||
|
||||
# PSO Peeps GC Plus USA / 3OE1
|
||||
# Source table: BattleParamEntry_on.dat
|
||||
# Active online battle-param table loaded at 0x811AB7C0
|
||||
# EXP field offset within each 0x24-byte row is +0x1C
|
||||
# Generated from clean BattleParamEntry_on.dat; multiplier=5x
|
||||
|
||||
.data 0x811AB7DC
|
||||
.data 4
|
||||
.binary 00000005
|
||||
|
||||
.data 0x811AB800
|
||||
.data 4
|
||||
.binary 0000001e
|
||||
|
||||
.data 0x811AB824
|
||||
.data 4
|
||||
.binary 00000019
|
||||
|
||||
.data 0x811AB848
|
||||
.data 4
|
||||
.binary 00000023
|
||||
|
||||
.data 0x811AB86C
|
||||
.data 4
|
||||
.binary 00000032
|
||||
|
||||
.data 0x811AB890
|
||||
.data 4
|
||||
.binary 000002ee
|
||||
|
||||
.data 0x811AB8B4
|
||||
.data 4
|
||||
.binary 0000007d
|
||||
|
||||
.data 0x811AB8D8
|
||||
.data 4
|
||||
.binary 00000050
|
||||
|
||||
.data 0x811AB8FC
|
||||
.data 4
|
||||
.binary 00000050
|
||||
|
||||
.data 0x811AB920
|
||||
.data 4
|
||||
.binary 00000055
|
||||
|
||||
.data 0x811AB944
|
||||
.data 4
|
||||
.binary 00000091
|
||||
|
||||
.data 0x811AB968
|
||||
.data 4
|
||||
.binary 00000014
|
||||
|
||||
.data 0x811AB98C
|
||||
.data 4
|
||||
.binary 00000014
|
||||
|
||||
.data 0x811AB9B0
|
||||
.data 4
|
||||
.binary 000000af
|
||||
|
||||
.data 0x811AB9D4
|
||||
.data 4
|
||||
.binary 000000af
|
||||
|
||||
.data 0x811AB9F8
|
||||
.data 4
|
||||
.binary 00001162
|
||||
|
||||
.data 0x811ABA1C
|
||||
.data 4
|
||||
.binary 00000032
|
||||
|
||||
.data 0x811ABA40
|
||||
.data 4
|
||||
.binary 00000032
|
||||
|
||||
.data 0x811ABA64
|
||||
.data 4
|
||||
.binary 000007d0
|
||||
|
||||
.data 0x811ABA88
|
||||
.data 4
|
||||
.binary 0000008c
|
||||
|
||||
.data 0x811ABB3C
|
||||
.data 4
|
||||
.binary 00000014
|
||||
|
||||
.data 0x811ABB60
|
||||
.data 4
|
||||
.binary 000001f4
|
||||
|
||||
.data 0x811ABB84
|
||||
.data 4
|
||||
.binary 0000004b
|
||||
|
||||
.data 0x811ABBA8
|
||||
.data 4
|
||||
.binary 0000000f
|
||||
|
||||
.data 0x811ABBCC
|
||||
.data 4
|
||||
.binary 0000005a
|
||||
|
||||
.data 0x811ABBF0
|
||||
.data 4
|
||||
.binary 0000006e
|
||||
|
||||
.data 0x811ABC14
|
||||
.data 4
|
||||
.binary 00000064
|
||||
|
||||
.data 0x811ABC38
|
||||
.data 4
|
||||
.binary 00000087
|
||||
|
||||
.data 0x811ABC5C
|
||||
.data 4
|
||||
.binary 0000001e
|
||||
|
||||
.data 0x811ABD10
|
||||
.data 4
|
||||
.binary 0000186a
|
||||
|
||||
.data 0x811ABE9C
|
||||
.data 4
|
||||
.binary 00000032
|
||||
|
||||
.data 0x811ABEC0
|
||||
.data 4
|
||||
.binary 0000001e
|
||||
|
||||
.data 0x811ABEE4
|
||||
.data 4
|
||||
.binary 00000014
|
||||
|
||||
.data 0x811ABF08
|
||||
.data 4
|
||||
.binary 00000014
|
||||
|
||||
.data 0x811ABF2C
|
||||
.data 4
|
||||
.binary 000002ee
|
||||
|
||||
.data 0x811ABF50
|
||||
.data 4
|
||||
.binary 00000019
|
||||
|
||||
.data 0x811ABF98
|
||||
.data 4
|
||||
.binary 00003a98
|
||||
|
||||
.data 0x811ABFE0
|
||||
.data 4
|
||||
.binary 00000019
|
||||
|
||||
.data 0x811AC220
|
||||
.data 4
|
||||
.binary 00000041
|
||||
|
||||
.data 0x811AC244
|
||||
.data 4
|
||||
.binary 000001f4
|
||||
|
||||
.data 0x811AC268
|
||||
.data 4
|
||||
.binary 00000019
|
||||
|
||||
.data 0x811AC28C
|
||||
.data 4
|
||||
.binary 0000001e
|
||||
|
||||
.data 0x811AC2B0
|
||||
.data 4
|
||||
.binary 00000023
|
||||
|
||||
.data 0x811AC2D4
|
||||
.data 4
|
||||
.binary 00000064
|
||||
|
||||
.data 0x811AC2F8
|
||||
.data 4
|
||||
.binary 00000032
|
||||
|
||||
.data 0x811AC31C
|
||||
.data 4
|
||||
.binary 0000003c
|
||||
|
||||
.data 0x811AC340
|
||||
.data 4
|
||||
.binary 00000046
|
||||
|
||||
.data 0x811AC364
|
||||
.data 4
|
||||
.binary 00000096
|
||||
|
||||
.data 0x811AC388
|
||||
.data 4
|
||||
.binary 0000006e
|
||||
|
||||
.data 0x811AC3AC
|
||||
.data 4
|
||||
.binary 00000078
|
||||
|
||||
.data 0x811AC3D0
|
||||
.data 4
|
||||
.binary 00000082
|
||||
|
||||
.data 0x811AC55C
|
||||
.data 4
|
||||
.binary 000000b4
|
||||
|
||||
.data 0x811AC580
|
||||
.data 4
|
||||
.binary 000000d7
|
||||
|
||||
.data 0x811AC5A4
|
||||
.data 4
|
||||
.binary 000000d2
|
||||
|
||||
.data 0x811AC5C8
|
||||
.data 4
|
||||
.binary 000000e1
|
||||
|
||||
.data 0x811AC5EC
|
||||
.data 4
|
||||
.binary 000000f5
|
||||
|
||||
.data 0x811AC610
|
||||
.data 4
|
||||
.binary 0000050f
|
||||
|
||||
.data 0x811AC634
|
||||
.data 4
|
||||
.binary 0000015e
|
||||
|
||||
.data 0x811AC658
|
||||
.data 4
|
||||
.binary 00000122
|
||||
|
||||
.data 0x811AC67C
|
||||
.data 4
|
||||
.binary 00000122
|
||||
|
||||
.data 0x811AC6A0
|
||||
.data 4
|
||||
.binary 0000012c
|
||||
|
||||
.data 0x811AC6C4
|
||||
.data 4
|
||||
.binary 00000186
|
||||
|
||||
.data 0x811AC6E8
|
||||
.data 4
|
||||
.binary 000000c8
|
||||
|
||||
.data 0x811AC70C
|
||||
.data 4
|
||||
.binary 000000c8
|
||||
|
||||
.data 0x811AC730
|
||||
.data 4
|
||||
.binary 000001e5
|
||||
|
||||
.data 0x811AC754
|
||||
.data 4
|
||||
.binary 000001a4
|
||||
|
||||
.data 0x811AC778
|
||||
.data 4
|
||||
.binary 00003e80
|
||||
|
||||
.data 0x811AC79C
|
||||
.data 4
|
||||
.binary 0000007d
|
||||
|
||||
.data 0x811AC7C0
|
||||
.data 4
|
||||
.binary 00000014
|
||||
|
||||
.data 0x811AC7E4
|
||||
.data 4
|
||||
.binary 00002ee0
|
||||
|
||||
.data 0x811AC808
|
||||
.data 4
|
||||
.binary 00000177
|
||||
|
||||
.data 0x811AC8BC
|
||||
.data 4
|
||||
.binary 000000c8
|
||||
|
||||
.data 0x811AC8E0
|
||||
.data 4
|
||||
.binary 00000398
|
||||
|
||||
.data 0x811AC904
|
||||
.data 4
|
||||
.binary 0000011d
|
||||
|
||||
.data 0x811AC928
|
||||
.data 4
|
||||
.binary 00000037
|
||||
|
||||
.data 0x811AC94C
|
||||
.data 4
|
||||
.binary 00000131
|
||||
|
||||
.data 0x811AC970
|
||||
.data 4
|
||||
.binary 0000014f
|
||||
|
||||
.data 0x811AC994
|
||||
.data 4
|
||||
.binary 00000140
|
||||
|
||||
.data 0x811AC9B8
|
||||
.data 4
|
||||
.binary 00000181
|
||||
|
||||
.data 0x811AC9DC
|
||||
.data 4
|
||||
.binary 000000d7
|
||||
|
||||
.data 0x811ACA90
|
||||
.data 4
|
||||
.binary 00004a38
|
||||
|
||||
.data 0x811ACC1C
|
||||
.data 4
|
||||
.binary 000000f5
|
||||
|
||||
.data 0x811ACC40
|
||||
.data 4
|
||||
.binary 00000082
|
||||
|
||||
.data 0x811ACC64
|
||||
.data 4
|
||||
.binary 00000073
|
||||
|
||||
.data 0x811ACC88
|
||||
.data 4
|
||||
.binary 00000073
|
||||
|
||||
.data 0x811ACCAC
|
||||
.data 4
|
||||
.binary 0000050f
|
||||
|
||||
.data 0x811ACCD0
|
||||
.data 4
|
||||
.binary 00000019
|
||||
|
||||
.data 0x811ACD3C
|
||||
.data 4
|
||||
.binary 00009c40
|
||||
|
||||
.data 0x811ACD60
|
||||
.data 4
|
||||
.binary 00000019
|
||||
|
||||
.data 0x811ACFA0
|
||||
.data 4
|
||||
.binary 00000118
|
||||
|
||||
.data 0x811ACFC4
|
||||
.data 4
|
||||
.binary 00000398
|
||||
|
||||
.data 0x811ACFE8
|
||||
.data 4
|
||||
.binary 000000d2
|
||||
|
||||
.data 0x811AD00C
|
||||
.data 4
|
||||
.binary 000000d7
|
||||
|
||||
.data 0x811AD030
|
||||
.data 4
|
||||
.binary 000000e1
|
||||
|
||||
.data 0x811AD054
|
||||
.data 4
|
||||
.binary 00000140
|
||||
|
||||
.data 0x811AD078
|
||||
.data 4
|
||||
.binary 000000f5
|
||||
|
||||
.data 0x811AD09C
|
||||
.data 4
|
||||
.binary 00000104
|
||||
|
||||
.data 0x811AD0C0
|
||||
.data 4
|
||||
.binary 00000113
|
||||
|
||||
.data 0x811AD0E4
|
||||
.data 4
|
||||
.binary 00000195
|
||||
|
||||
.data 0x811AD108
|
||||
.data 4
|
||||
.binary 0000014f
|
||||
|
||||
.data 0x811AD12C
|
||||
.data 4
|
||||
.binary 0000015e
|
||||
|
||||
.data 0x811AD150
|
||||
.data 4
|
||||
.binary 0000016d
|
||||
|
||||
.data 0x811AD2DC
|
||||
.data 4
|
||||
.binary 0000019a
|
||||
|
||||
.data 0x811AD300
|
||||
.data 4
|
||||
.binary 000001cc
|
||||
|
||||
.data 0x811AD324
|
||||
.data 4
|
||||
.binary 000001c2
|
||||
|
||||
.data 0x811AD348
|
||||
.data 4
|
||||
.binary 000001d6
|
||||
|
||||
.data 0x811AD36C
|
||||
.data 4
|
||||
.binary 000001f4
|
||||
|
||||
.data 0x811AD390
|
||||
.data 4
|
||||
.binary 0000076c
|
||||
|
||||
.data 0x811AD3B4
|
||||
.data 4
|
||||
.binary 000002ad
|
||||
|
||||
.data 0x811AD3D8
|
||||
.data 4
|
||||
.binary 00000230
|
||||
|
||||
.data 0x811AD3FC
|
||||
.data 4
|
||||
.binary 00000230
|
||||
|
||||
.data 0x811AD420
|
||||
.data 4
|
||||
.binary 0000023a
|
||||
|
||||
.data 0x811AD444
|
||||
.data 4
|
||||
.binary 000002e4
|
||||
|
||||
.data 0x811AD468
|
||||
.data 4
|
||||
.binary 000001b8
|
||||
|
||||
.data 0x811AD48C
|
||||
.data 4
|
||||
.binary 000001b8
|
||||
|
||||
.data 0x811AD4B0
|
||||
.data 4
|
||||
.binary 000002ee
|
||||
|
||||
.data 0x811AD4D4
|
||||
.data 4
|
||||
.binary 000002d0
|
||||
|
||||
.data 0x811AD4F8
|
||||
.data 4
|
||||
.binary 0000a9ec
|
||||
|
||||
.data 0x811AD51C
|
||||
.data 4
|
||||
.binary 00000096
|
||||
|
||||
.data 0x811AD540
|
||||
.data 4
|
||||
.binary 00000028
|
||||
|
||||
.data 0x811AD564
|
||||
.data 4
|
||||
.binary 00009bdc
|
||||
|
||||
.data 0x811AD588
|
||||
.data 4
|
||||
.binary 000002cb
|
||||
|
||||
.data 0x811AD63C
|
||||
.data 4
|
||||
.binary 000001b8
|
||||
|
||||
.data 0x811AD660
|
||||
.data 4
|
||||
.binary 00000578
|
||||
|
||||
.data 0x811AD684
|
||||
.data 4
|
||||
.binary 00000258
|
||||
|
||||
.data 0x811AD6A8
|
||||
.data 4
|
||||
.binary 0000006e
|
||||
|
||||
.data 0x811AD6CC
|
||||
.data 4
|
||||
.binary 00000244
|
||||
|
||||
.data 0x811AD6F0
|
||||
.data 4
|
||||
.binary 0000026c
|
||||
|
||||
.data 0x811AD714
|
||||
.data 4
|
||||
.binary 00000258
|
||||
|
||||
.data 0x811AD738
|
||||
.data 4
|
||||
.binary 00000280
|
||||
|
||||
.data 0x811AD75C
|
||||
.data 4
|
||||
.binary 000001cc
|
||||
|
||||
.data 0x811AD810
|
||||
.data 4
|
||||
.binary 0000d8cc
|
||||
|
||||
.data 0x811AD99C
|
||||
.data 4
|
||||
.binary 000001f4
|
||||
|
||||
.data 0x811AD9C0
|
||||
.data 4
|
||||
.binary 00000104
|
||||
|
||||
.data 0x811AD9E4
|
||||
.data 4
|
||||
.binary 000000f0
|
||||
|
||||
.data 0x811ADA08
|
||||
.data 4
|
||||
.binary 000000f0
|
||||
|
||||
.data 0x811ADA2C
|
||||
.data 4
|
||||
.binary 0000076c
|
||||
|
||||
.data 0x811ADA50
|
||||
.data 4
|
||||
.binary 0000002d
|
||||
|
||||
.data 0x811ADABC
|
||||
.data 4
|
||||
.binary 00013880
|
||||
|
||||
.data 0x811ADAE0
|
||||
.data 4
|
||||
.binary 0000002d
|
||||
|
||||
.data 0x811ADD20
|
||||
.data 4
|
||||
.binary 0000022b
|
||||
|
||||
.data 0x811ADD44
|
||||
.data 4
|
||||
.binary 00000578
|
||||
|
||||
.data 0x811ADD68
|
||||
.data 4
|
||||
.binary 000001c2
|
||||
|
||||
.data 0x811ADD8C
|
||||
.data 4
|
||||
.binary 000001cc
|
||||
|
||||
.data 0x811ADDB0
|
||||
.data 4
|
||||
.binary 000001d6
|
||||
|
||||
.data 0x811ADDD4
|
||||
.data 4
|
||||
.binary 00000271
|
||||
|
||||
.data 0x811ADDF8
|
||||
.data 4
|
||||
.binary 000001f4
|
||||
|
||||
.data 0x811ADE1C
|
||||
.data 4
|
||||
.binary 00000208
|
||||
|
||||
.data 0x811ADE40
|
||||
.data 4
|
||||
.binary 0000021c
|
||||
|
||||
.data 0x811ADE64
|
||||
.data 4
|
||||
.binary 000002ee
|
||||
|
||||
.data 0x811ADE88
|
||||
.data 4
|
||||
.binary 0000026c
|
||||
|
||||
.data 0x811ADEAC
|
||||
.data 4
|
||||
.binary 00000280
|
||||
|
||||
.data 0x811ADED0
|
||||
.data 4
|
||||
.binary 00000294
|
||||
|
||||
.data 0x811AE05C
|
||||
.data 4
|
||||
.binary 000002d5
|
||||
|
||||
.data 0x811AE080
|
||||
.data 4
|
||||
.binary 00000546
|
||||
|
||||
.data 0x811AE0A4
|
||||
.data 4
|
||||
.binary 0000055f
|
||||
|
||||
.data 0x811AE0C8
|
||||
.data 4
|
||||
.binary 00000578
|
||||
|
||||
.data 0x811AE0EC
|
||||
.data 4
|
||||
.binary 000005c3
|
||||
|
||||
.data 0x811AE110
|
||||
.data 4
|
||||
.binary 00001194
|
||||
|
||||
.data 0x811AE134
|
||||
.data 4
|
||||
.binary 000006d6
|
||||
|
||||
.data 0x811AE158
|
||||
.data 4
|
||||
.binary 00000640
|
||||
|
||||
.data 0x811AE17C
|
||||
.data 4
|
||||
.binary 00000640
|
||||
|
||||
.data 0x811AE1A0
|
||||
.data 4
|
||||
.binary 00000659
|
||||
|
||||
.data 0x811AE1C4
|
||||
.data 4
|
||||
.binary 00000717
|
||||
|
||||
.data 0x811AE1E8
|
||||
.data 4
|
||||
.binary 000001f4
|
||||
|
||||
.data 0x811AE20C
|
||||
.data 4
|
||||
.binary 000001f4
|
||||
|
||||
.data 0x811AE230
|
||||
.data 4
|
||||
.binary 00000785
|
||||
|
||||
.data 0x811AE254
|
||||
.data 4
|
||||
.binary 00000744
|
||||
|
||||
.data 0x811AE278
|
||||
.data 4
|
||||
.binary 00016f30
|
||||
|
||||
.data 0x811AE29C
|
||||
.data 4
|
||||
.binary 0000012c
|
||||
|
||||
.data 0x811AE2C0
|
||||
.data 4
|
||||
.binary 00000050
|
||||
|
||||
.data 0x811AE2E4
|
||||
.data 4
|
||||
.binary 0001368c
|
||||
|
||||
.data 0x811AE308
|
||||
.data 4
|
||||
.binary 000006b8
|
||||
|
||||
.data 0x811AE3BC
|
||||
.data 4
|
||||
.binary 00000500
|
||||
|
||||
.data 0x811AE3E0
|
||||
.data 4
|
||||
.binary 00000dac
|
||||
|
||||
.data 0x811AE404
|
||||
.data 4
|
||||
.binary 00000640
|
||||
|
||||
.data 0x811AE428
|
||||
.data 4
|
||||
.binary 00000127
|
||||
|
||||
.data 0x811AE44C
|
||||
.data 4
|
||||
.binary 00000645
|
||||
|
||||
.data 0x811AE470
|
||||
.data 4
|
||||
.binary 000006ef
|
||||
|
||||
.data 0x811AE494
|
||||
.data 4
|
||||
.binary 00000686
|
||||
|
||||
.data 0x811AE4B8
|
||||
.data 4
|
||||
.binary 00000686
|
||||
|
||||
.data 0x811AE4DC
|
||||
.data 4
|
||||
.binary 00000555
|
||||
|
||||
.data 0x811AE590
|
||||
.data 4
|
||||
.binary 0001cafc
|
||||
|
||||
.data 0x811AE71C
|
||||
.data 4
|
||||
.binary 000005aa
|
||||
|
||||
.data 0x811AE740
|
||||
.data 4
|
||||
.binary 000003cf
|
||||
|
||||
.data 0x811AE764
|
||||
.data 4
|
||||
.binary 00000384
|
||||
|
||||
.data 0x811AE788
|
||||
.data 4
|
||||
.binary 00000384
|
||||
|
||||
.data 0x811AE7AC
|
||||
.data 4
|
||||
.binary 00001130
|
||||
|
||||
.data 0x811AE7D0
|
||||
.data 4
|
||||
.binary 0000007d
|
||||
|
||||
.data 0x811AE83C
|
||||
.data 4
|
||||
.binary 0003d090
|
||||
|
||||
.data 0x811AE860
|
||||
.data 4
|
||||
.binary 00000064
|
||||
|
||||
.data 0x811AEAA0
|
||||
.data 4
|
||||
.binary 000005dc
|
||||
|
||||
.data 0x811AEAC4
|
||||
.data 4
|
||||
.binary 00000d7a
|
||||
|
||||
.data 0x811AEAE8
|
||||
.data 4
|
||||
.binary 0000054b
|
||||
|
||||
.data 0x811AEB0C
|
||||
.data 4
|
||||
.binary 00000564
|
||||
|
||||
.data 0x811AEB30
|
||||
.data 4
|
||||
.binary 0000053c
|
||||
|
||||
.data 0x811AEB54
|
||||
.data 4
|
||||
.binary 00000681
|
||||
|
||||
.data 0x811AEB78
|
||||
.data 4
|
||||
.binary 000005c3
|
||||
|
||||
.data 0x811AEB9C
|
||||
.data 4
|
||||
.binary 000005eb
|
||||
|
||||
.data 0x811AEBC0
|
||||
.data 4
|
||||
.binary 00000636
|
||||
|
||||
.data 0x811AEBE4
|
||||
.data 4
|
||||
.binary 00000753
|
||||
|
||||
.data 0x811AEC08
|
||||
.data 4
|
||||
.binary 0000069f
|
||||
|
||||
.data 0x811AEC2C
|
||||
.data 4
|
||||
.binary 000006bd
|
||||
|
||||
.data 0x811AEC50
|
||||
.data 4
|
||||
.binary 000006f4
|
||||
|
||||
.data 0
|
||||
.data 0
|
||||
@@ -0,0 +1,974 @@
|
||||
.meta name="PSO Peeps EP2 10x EXP"
|
||||
.meta description="Sets EP2 enemy EXP\nto 10x for GC crossplay"
|
||||
|
||||
entry_ptr:
|
||||
reloc0:
|
||||
.offsetof start
|
||||
|
||||
start:
|
||||
.include WriteCodeBlocksGC
|
||||
|
||||
# PSO Peeps GC Plus USA / 3OE1
|
||||
# Source table: BattleParamEntry_lab_on.dat
|
||||
# Active online battle-param table loaded at 0x811AB7C0
|
||||
# EXP field offset within each 0x24-byte row is +0x1C
|
||||
# Generated from clean BattleParamEntry_lab_on.dat; multiplier=10x
|
||||
|
||||
.data 0x811AB7DC
|
||||
.data 4
|
||||
.binary 0000000a
|
||||
|
||||
.data 0x811AB800
|
||||
.data 4
|
||||
.binary 0000003c
|
||||
|
||||
.data 0x811AB824
|
||||
.data 4
|
||||
.binary 00000064
|
||||
|
||||
.data 0x811AB848
|
||||
.data 4
|
||||
.binary 00000078
|
||||
|
||||
.data 0x811AB86C
|
||||
.data 4
|
||||
.binary 00000064
|
||||
|
||||
.data 0x811AB890
|
||||
.data 4
|
||||
.binary 000005dc
|
||||
|
||||
.data 0x811AB8B4
|
||||
.data 4
|
||||
.binary 000000be
|
||||
|
||||
.data 0x811AB8D8
|
||||
.data 4
|
||||
.binary 00000064
|
||||
|
||||
.data 0x811AB8FC
|
||||
.data 4
|
||||
.binary 000000a0
|
||||
|
||||
.data 0x811AB920
|
||||
.data 4
|
||||
.binary 000000aa
|
||||
|
||||
.data 0x811AB944
|
||||
.data 4
|
||||
.binary 000000aa
|
||||
|
||||
.data 0x811AB968
|
||||
.data 4
|
||||
.binary 00000014
|
||||
|
||||
.data 0x811AB98C
|
||||
.data 4
|
||||
.binary 00000014
|
||||
|
||||
.data 0x811AB9B0
|
||||
.data 4
|
||||
.binary 00000190
|
||||
|
||||
.data 0x811AB9D4
|
||||
.data 4
|
||||
.binary 00000096
|
||||
|
||||
.data 0x811AB9F8
|
||||
.data 4
|
||||
.binary 000012c0
|
||||
|
||||
.data 0x811ABA1C
|
||||
.data 4
|
||||
.binary 00000064
|
||||
|
||||
.data 0x811ABA40
|
||||
.data 4
|
||||
.binary 0000001e
|
||||
|
||||
.data 0x811ABA64
|
||||
.data 4
|
||||
.binary 0000251c
|
||||
|
||||
.data 0x811ABA88
|
||||
.data 4
|
||||
.binary 000000d2
|
||||
|
||||
.data 0x811ABB3C
|
||||
.data 4
|
||||
.binary 00000028
|
||||
|
||||
.data 0x811ABB60
|
||||
.data 4
|
||||
.binary 00000a00
|
||||
|
||||
.data 0x811ABB84
|
||||
.data 4
|
||||
.binary 000001fe
|
||||
|
||||
.data 0x811ABBA8
|
||||
.data 4
|
||||
.binary 00000014
|
||||
|
||||
.data 0x811ABBCC
|
||||
.data 4
|
||||
.binary 0000006e
|
||||
|
||||
.data 0x811ABBF0
|
||||
.data 4
|
||||
.binary 000000be
|
||||
|
||||
.data 0x811ABC14
|
||||
.data 4
|
||||
.binary 00003a98
|
||||
|
||||
.data 0x811ABCC8
|
||||
.data 4
|
||||
.binary 00000550
|
||||
|
||||
.data 0x811ABD10
|
||||
.data 4
|
||||
.binary 000001ea
|
||||
|
||||
.data 0x811ABD34
|
||||
.data 4
|
||||
.binary 000001c2
|
||||
|
||||
.data 0x811ABE0C
|
||||
.data 4
|
||||
.binary 000080e8
|
||||
|
||||
.data 0x811ABE9C
|
||||
.data 4
|
||||
.binary 00000122
|
||||
|
||||
.data 0x811ABEC0
|
||||
.data 4
|
||||
.binary 0000001e
|
||||
|
||||
.data 0x811ABEE4
|
||||
.data 4
|
||||
.binary 00000046
|
||||
|
||||
.data 0x811ABF08
|
||||
.data 4
|
||||
.binary 00000050
|
||||
|
||||
.data 0x811AC004
|
||||
.data 4
|
||||
.binary 000001d6
|
||||
|
||||
.data 0x811AC028
|
||||
.data 4
|
||||
.binary 00000078
|
||||
|
||||
.data 0x811AC04C
|
||||
.data 4
|
||||
.binary 00000096
|
||||
|
||||
.data 0x811AC070
|
||||
.data 4
|
||||
.binary 00000208
|
||||
|
||||
.data 0x811AC0DC
|
||||
.data 4
|
||||
.binary 0000012c
|
||||
|
||||
.data 0x811AC100
|
||||
.data 4
|
||||
.binary 00000078
|
||||
|
||||
.data 0x811AC124
|
||||
.data 4
|
||||
.binary 00000014
|
||||
|
||||
.data 0x811AC148
|
||||
.data 4
|
||||
.binary 00000140
|
||||
|
||||
.data 0x811AC16C
|
||||
.data 4
|
||||
.binary 00000190
|
||||
|
||||
.data 0x811AC190
|
||||
.data 4
|
||||
.binary 000001f4
|
||||
|
||||
.data 0x811AC1B4
|
||||
.data 4
|
||||
.binary 00000226
|
||||
|
||||
.data 0x811AC220
|
||||
.data 4
|
||||
.binary 00000064
|
||||
|
||||
.data 0x811AC244
|
||||
.data 4
|
||||
.binary 000003e8
|
||||
|
||||
.data 0x811AC268
|
||||
.data 4
|
||||
.binary 0000006e
|
||||
|
||||
.data 0x811AC28C
|
||||
.data 4
|
||||
.binary 00000082
|
||||
|
||||
.data 0x811AC2B0
|
||||
.data 4
|
||||
.binary 00000046
|
||||
|
||||
.data 0x811AC2D4
|
||||
.data 4
|
||||
.binary 00000078
|
||||
|
||||
.data 0x811AC2F8
|
||||
.data 4
|
||||
.binary 000000b4
|
||||
|
||||
.data 0x811AC31C
|
||||
.data 4
|
||||
.binary 000000d2
|
||||
|
||||
.data 0x811AC340
|
||||
.data 4
|
||||
.binary 0000008c
|
||||
|
||||
.data 0x811AC364
|
||||
.data 4
|
||||
.binary 00000096
|
||||
|
||||
.data 0x811AC388
|
||||
.data 4
|
||||
.binary 0000003c
|
||||
|
||||
.data 0x811AC3AC
|
||||
.data 4
|
||||
.binary 00000046
|
||||
|
||||
.data 0x811AC3D0
|
||||
.data 4
|
||||
.binary 00000050
|
||||
|
||||
.data 0x811AC55C
|
||||
.data 4
|
||||
.binary 00000172
|
||||
|
||||
.data 0x811AC580
|
||||
.data 4
|
||||
.binary 000001c2
|
||||
|
||||
.data 0x811AC5A4
|
||||
.data 4
|
||||
.binary 00000208
|
||||
|
||||
.data 0x811AC5C8
|
||||
.data 4
|
||||
.binary 00000226
|
||||
|
||||
.data 0x811AC5EC
|
||||
.data 4
|
||||
.binary 00000208
|
||||
|
||||
.data 0x811AC610
|
||||
.data 4
|
||||
.binary 00000b0e
|
||||
|
||||
.data 0x811AC634
|
||||
.data 4
|
||||
.binary 00000294
|
||||
|
||||
.data 0x811AC658
|
||||
.data 4
|
||||
.binary 00000208
|
||||
|
||||
.data 0x811AC67C
|
||||
.data 4
|
||||
.binary 00000244
|
||||
|
||||
.data 0x811AC6A0
|
||||
.data 4
|
||||
.binary 00000258
|
||||
|
||||
.data 0x811AC6C4
|
||||
.data 4
|
||||
.binary 00000276
|
||||
|
||||
.data 0x811AC6E8
|
||||
.data 4
|
||||
.binary 0000017c
|
||||
|
||||
.data 0x811AC70C
|
||||
.data 4
|
||||
.binary 0000017c
|
||||
|
||||
.data 0x811AC730
|
||||
.data 4
|
||||
.binary 000003f2
|
||||
|
||||
.data 0x811AC754
|
||||
.data 4
|
||||
.binary 00000258
|
||||
|
||||
.data 0x811AC778
|
||||
.data 4
|
||||
.binary 00007d00
|
||||
|
||||
.data 0x811AC79C
|
||||
.data 4
|
||||
.binary 000000fa
|
||||
|
||||
.data 0x811AC7C0
|
||||
.data 4
|
||||
.binary 00000028
|
||||
|
||||
.data 0x811AC7E4
|
||||
.data 4
|
||||
.binary 00009858
|
||||
|
||||
.data 0x811AC808
|
||||
.data 4
|
||||
.binary 00000294
|
||||
|
||||
.data 0x811AC8BC
|
||||
.data 4
|
||||
.binary 000001a4
|
||||
|
||||
.data 0x811AC8E0
|
||||
.data 4
|
||||
.binary 00001400
|
||||
|
||||
.data 0x811AC904
|
||||
.data 4
|
||||
.binary 000004a6
|
||||
|
||||
.data 0x811AC928
|
||||
.data 4
|
||||
.binary 00000064
|
||||
|
||||
.data 0x811AC94C
|
||||
.data 4
|
||||
.binary 00000212
|
||||
|
||||
.data 0x811AC970
|
||||
.data 4
|
||||
.binary 00000294
|
||||
|
||||
.data 0x811AC994
|
||||
.data 4
|
||||
.binary 0000afc8
|
||||
|
||||
.data 0x811ACA48
|
||||
.data 4
|
||||
.binary 00000a1e
|
||||
|
||||
.data 0x811ACA90
|
||||
.data 4
|
||||
.binary 00000488
|
||||
|
||||
.data 0x811ACAB4
|
||||
.data 4
|
||||
.binary 00000442
|
||||
|
||||
.data 0x811ACB8C
|
||||
.data 4
|
||||
.binary 00012cc8
|
||||
|
||||
.data 0x811ACC1C
|
||||
.data 4
|
||||
.binary 0000033e
|
||||
|
||||
.data 0x811ACC40
|
||||
.data 4
|
||||
.binary 000001d6
|
||||
|
||||
.data 0x811ACC64
|
||||
.data 4
|
||||
.binary 000001d6
|
||||
|
||||
.data 0x811ACC88
|
||||
.data 4
|
||||
.binary 000001e0
|
||||
|
||||
.data 0x811ACD84
|
||||
.data 4
|
||||
.binary 0000046a
|
||||
|
||||
.data 0x811ACDA8
|
||||
.data 4
|
||||
.binary 00000226
|
||||
|
||||
.data 0x811ACDCC
|
||||
.data 4
|
||||
.binary 00000258
|
||||
|
||||
.data 0x811ACDF0
|
||||
.data 4
|
||||
.binary 000004ba
|
||||
|
||||
.data 0x811ACE5C
|
||||
.data 4
|
||||
.binary 00000352
|
||||
|
||||
.data 0x811ACE80
|
||||
.data 4
|
||||
.binary 00000226
|
||||
|
||||
.data 0x811ACEA4
|
||||
.data 4
|
||||
.binary 000001c2
|
||||
|
||||
.data 0x811ACEC8
|
||||
.data 4
|
||||
.binary 00000370
|
||||
|
||||
.data 0x811ACEEC
|
||||
.data 4
|
||||
.binary 000003f2
|
||||
|
||||
.data 0x811ACF10
|
||||
.data 4
|
||||
.binary 0000049c
|
||||
|
||||
.data 0x811ACF34
|
||||
.data 4
|
||||
.binary 000004ec
|
||||
|
||||
.data 0x811ACF7C
|
||||
.data 4
|
||||
.binary 00000032
|
||||
|
||||
.data 0x811ACFA0
|
||||
.data 4
|
||||
.binary 00000208
|
||||
|
||||
.data 0x811ACFC4
|
||||
.data 4
|
||||
.binary 000007d0
|
||||
|
||||
.data 0x811ACFE8
|
||||
.data 4
|
||||
.binary 00000212
|
||||
|
||||
.data 0x811AD00C
|
||||
.data 4
|
||||
.binary 00000230
|
||||
|
||||
.data 0x811AD030
|
||||
.data 4
|
||||
.binary 000001c2
|
||||
|
||||
.data 0x811AD054
|
||||
.data 4
|
||||
.binary 00000226
|
||||
|
||||
.data 0x811AD078
|
||||
.data 4
|
||||
.binary 0000028a
|
||||
|
||||
.data 0x811AD09C
|
||||
.data 4
|
||||
.binary 000002bc
|
||||
|
||||
.data 0x811AD0C0
|
||||
.data 4
|
||||
.binary 00000226
|
||||
|
||||
.data 0x811AD0E4
|
||||
.data 4
|
||||
.binary 00000258
|
||||
|
||||
.data 0x811AD108
|
||||
.data 4
|
||||
.binary 000001c2
|
||||
|
||||
.data 0x811AD12C
|
||||
.data 4
|
||||
.binary 000001d6
|
||||
|
||||
.data 0x811AD150
|
||||
.data 4
|
||||
.binary 000001e0
|
||||
|
||||
.data 0x811AD2DC
|
||||
.data 4
|
||||
.binary 00000366
|
||||
|
||||
.data 0x811AD300
|
||||
.data 4
|
||||
.binary 000003ca
|
||||
|
||||
.data 0x811AD324
|
||||
.data 4
|
||||
.binary 0000041a
|
||||
|
||||
.data 0x811AD348
|
||||
.data 4
|
||||
.binary 00000442
|
||||
|
||||
.data 0x811AD36C
|
||||
.data 4
|
||||
.binary 0000041a
|
||||
|
||||
.data 0x811AD390
|
||||
.data 4
|
||||
.binary 00000f0a
|
||||
|
||||
.data 0x811AD3B4
|
||||
.data 4
|
||||
.binary 0000047e
|
||||
|
||||
.data 0x811AD3D8
|
||||
.data 4
|
||||
.binary 0000041a
|
||||
|
||||
.data 0x811AD3FC
|
||||
.data 4
|
||||
.binary 00000460
|
||||
|
||||
.data 0x811AD420
|
||||
.data 4
|
||||
.binary 00000474
|
||||
|
||||
.data 0x811AD444
|
||||
.data 4
|
||||
.binary 000004a6
|
||||
|
||||
.data 0x811AD468
|
||||
.data 4
|
||||
.binary 0000037a
|
||||
|
||||
.data 0x811AD48C
|
||||
.data 4
|
||||
.binary 0000037a
|
||||
|
||||
.data 0x811AD4B0
|
||||
.data 4
|
||||
.binary 00000672
|
||||
|
||||
.data 0x811AD4D4
|
||||
.data 4
|
||||
.binary 0000047e
|
||||
|
||||
.data 0x811AD4F8
|
||||
.data 4
|
||||
.binary 000157c0
|
||||
|
||||
.data 0x811AD51C
|
||||
.data 4
|
||||
.binary 00000258
|
||||
|
||||
.data 0x811AD540
|
||||
.data 4
|
||||
.binary 00000258
|
||||
|
||||
.data 0x811AD564
|
||||
.data 4
|
||||
.binary 000186a0
|
||||
|
||||
.data 0x811AD588
|
||||
.data 4
|
||||
.binary 000004ce
|
||||
|
||||
.data 0x811AD63C
|
||||
.data 4
|
||||
.binary 000003a2
|
||||
|
||||
.data 0x811AD660
|
||||
.data 4
|
||||
.binary 00001d88
|
||||
|
||||
.data 0x811AD684
|
||||
.data 4
|
||||
.binary 0000074e
|
||||
|
||||
.data 0x811AD6A8
|
||||
.data 4
|
||||
.binary 0000012c
|
||||
|
||||
.data 0x811AD6CC
|
||||
.data 4
|
||||
.binary 0000042e
|
||||
|
||||
.data 0x811AD6F0
|
||||
.data 4
|
||||
.binary 000004ce
|
||||
|
||||
.data 0x811AD714
|
||||
.data 4
|
||||
.binary 0001e848
|
||||
|
||||
.data 0x811AD7C8
|
||||
.data 4
|
||||
.binary 00000df2
|
||||
|
||||
.data 0x811AD810
|
||||
.data 4
|
||||
.binary 00000726
|
||||
|
||||
.data 0x811AD834
|
||||
.data 4
|
||||
.binary 000006d6
|
||||
|
||||
.data 0x811AD90C
|
||||
.data 4
|
||||
.binary 000249f0
|
||||
|
||||
.data 0x811AD99C
|
||||
.data 4
|
||||
.binary 00000596
|
||||
|
||||
.data 0x811AD9C0
|
||||
.data 4
|
||||
.binary 0000038e
|
||||
|
||||
.data 0x811AD9E4
|
||||
.data 4
|
||||
.binary 000003de
|
||||
|
||||
.data 0x811ADA08
|
||||
.data 4
|
||||
.binary 000003f2
|
||||
|
||||
.data 0x811ADB04
|
||||
.data 4
|
||||
.binary 000006fe
|
||||
|
||||
.data 0x811ADB28
|
||||
.data 4
|
||||
.binary 00000442
|
||||
|
||||
.data 0x811ADB4C
|
||||
.data 4
|
||||
.binary 0000047e
|
||||
|
||||
.data 0x811ADB70
|
||||
.data 4
|
||||
.binary 00000762
|
||||
|
||||
.data 0x811ADBDC
|
||||
.data 4
|
||||
.binary 000005aa
|
||||
|
||||
.data 0x811ADC00
|
||||
.data 4
|
||||
.binary 00000442
|
||||
|
||||
.data 0x811ADC24
|
||||
.data 4
|
||||
.binary 000003b6
|
||||
|
||||
.data 0x811ADC48
|
||||
.data 4
|
||||
.binary 000005d2
|
||||
|
||||
.data 0x811ADC6C
|
||||
.data 4
|
||||
.binary 00000672
|
||||
|
||||
.data 0x811ADC90
|
||||
.data 4
|
||||
.binary 0000073a
|
||||
|
||||
.data 0x811ADCB4
|
||||
.data 4
|
||||
.binary 0000079e
|
||||
|
||||
.data 0x811ADCFC
|
||||
.data 4
|
||||
.binary 00000032
|
||||
|
||||
.data 0x811ADD20
|
||||
.data 4
|
||||
.binary 0000041a
|
||||
|
||||
.data 0x811ADD44
|
||||
.data 4
|
||||
.binary 00000b22
|
||||
|
||||
.data 0x811ADD68
|
||||
.data 4
|
||||
.binary 0000042e
|
||||
|
||||
.data 0x811ADD8C
|
||||
.data 4
|
||||
.binary 00000456
|
||||
|
||||
.data 0x811ADDB0
|
||||
.data 4
|
||||
.binary 000003ac
|
||||
|
||||
.data 0x811ADDD4
|
||||
.data 4
|
||||
.binary 00000442
|
||||
|
||||
.data 0x811ADDF8
|
||||
.data 4
|
||||
.binary 000004ba
|
||||
|
||||
.data 0x811ADE1C
|
||||
.data 4
|
||||
.binary 000004f6
|
||||
|
||||
.data 0x811ADE40
|
||||
.data 4
|
||||
.binary 00000438
|
||||
|
||||
.data 0x811ADE64
|
||||
.data 4
|
||||
.binary 0000047e
|
||||
|
||||
.data 0x811ADE88
|
||||
.data 4
|
||||
.binary 000003ca
|
||||
|
||||
.data 0x811ADEAC
|
||||
.data 4
|
||||
.binary 000003de
|
||||
|
||||
.data 0x811ADED0
|
||||
.data 4
|
||||
.binary 000003f2
|
||||
|
||||
.data 0x811AE05C
|
||||
.data 4
|
||||
.binary 000005dc
|
||||
|
||||
.data 0x811AE080
|
||||
.data 4
|
||||
.binary 00000bf4
|
||||
|
||||
.data 0x811AE0A4
|
||||
.data 4
|
||||
.binary 00000ce4
|
||||
|
||||
.data 0x811AE0C8
|
||||
.data 4
|
||||
.binary 00000d5c
|
||||
|
||||
.data 0x811AE0EC
|
||||
.data 4
|
||||
.binary 00000ce4
|
||||
|
||||
.data 0x811AE110
|
||||
.data 4
|
||||
.binary 00002db4
|
||||
|
||||
.data 0x811AE134
|
||||
.data 4
|
||||
.binary 00000e10
|
||||
|
||||
.data 0x811AE158
|
||||
.data 4
|
||||
.binary 00000ce4
|
||||
|
||||
.data 0x811AE17C
|
||||
.data 4
|
||||
.binary 00000be0
|
||||
|
||||
.data 0x811AE1A0
|
||||
.data 4
|
||||
.binary 00000c08
|
||||
|
||||
.data 0x811AE1C4
|
||||
.data 4
|
||||
.binary 00000e88
|
||||
|
||||
.data 0x811AE1E8
|
||||
.data 4
|
||||
.binary 000003e8
|
||||
|
||||
.data 0x811AE20C
|
||||
.data 4
|
||||
.binary 000003e8
|
||||
|
||||
.data 0x811AE230
|
||||
.data 4
|
||||
.binary 000013ec
|
||||
|
||||
.data 0x811AE254
|
||||
.data 4
|
||||
.binary 00000e10
|
||||
|
||||
.data 0x811AE278
|
||||
.data 4
|
||||
.binary 0002bf20
|
||||
|
||||
.data 0x811AE29C
|
||||
.data 4
|
||||
.binary 00000258
|
||||
|
||||
.data 0x811AE2C0
|
||||
.data 4
|
||||
.binary 000000a0
|
||||
|
||||
.data 0x811AE2E4
|
||||
.data 4
|
||||
.binary 00033450
|
||||
|
||||
.data 0x811AE308
|
||||
.data 4
|
||||
.binary 00000f00
|
||||
|
||||
.data 0x811AE3BC
|
||||
.data 4
|
||||
.binary 00000b7c
|
||||
|
||||
.data 0x811AE3E0
|
||||
.data 4
|
||||
.binary 00002800
|
||||
|
||||
.data 0x811AE404
|
||||
.data 4
|
||||
.binary 00001680
|
||||
|
||||
.data 0x811AE428
|
||||
.data 4
|
||||
.binary 000001f4
|
||||
|
||||
.data 0x811AE44C
|
||||
.data 4
|
||||
.binary 00000d20
|
||||
|
||||
.data 0x811AE470
|
||||
.data 4
|
||||
.binary 00000f00
|
||||
|
||||
.data 0x811AE494
|
||||
.data 4
|
||||
.binary 0003c4d8
|
||||
|
||||
.data 0x811AE548
|
||||
.data 4
|
||||
.binary 00002a6c
|
||||
|
||||
.data 0x811AE590
|
||||
.data 4
|
||||
.binary 00001608
|
||||
|
||||
.data 0x811AE5B4
|
||||
.data 4
|
||||
.binary 00001518
|
||||
|
||||
.data 0x811AE68C
|
||||
.data 4
|
||||
.binary 00073f78
|
||||
|
||||
.data 0x811AE71C
|
||||
.data 4
|
||||
.binary 00001158
|
||||
|
||||
.data 0x811AE740
|
||||
.data 4
|
||||
.binary 00000c30
|
||||
|
||||
.data 0x811AE764
|
||||
.data 4
|
||||
.binary 00000c30
|
||||
|
||||
.data 0x811AE788
|
||||
.data 4
|
||||
.binary 00000c6c
|
||||
|
||||
.data 0x811AE884
|
||||
.data 4
|
||||
.binary 00000d98
|
||||
|
||||
.data 0x811AE8A8
|
||||
.data 4
|
||||
.binary 00000d5c
|
||||
|
||||
.data 0x811AE8CC
|
||||
.data 4
|
||||
.binary 00000e10
|
||||
|
||||
.data 0x811AE8F0
|
||||
.data 4
|
||||
.binary 000016bc
|
||||
|
||||
.data 0x811AE95C
|
||||
.data 4
|
||||
.binary 00001194
|
||||
|
||||
.data 0x811AE980
|
||||
.data 4
|
||||
.binary 00000d5c
|
||||
|
||||
.data 0x811AE9A4
|
||||
.data 4
|
||||
.binary 000007d0
|
||||
|
||||
.data 0x811AE9C8
|
||||
.data 4
|
||||
.binary 0000120c
|
||||
|
||||
.data 0x811AE9EC
|
||||
.data 4
|
||||
.binary 000013ec
|
||||
|
||||
.data 0x811AEA10
|
||||
.data 4
|
||||
.binary 00001644
|
||||
|
||||
.data 0x811AEA34
|
||||
.data 4
|
||||
.binary 00001770
|
||||
|
||||
.data 0x811AEA7C
|
||||
.data 4
|
||||
.binary 00000032
|
||||
|
||||
.data 0x811AEAA0
|
||||
.data 4
|
||||
.binary 00000ce4
|
||||
|
||||
.data 0x811AEAC4
|
||||
.data 4
|
||||
.binary 000021fc
|
||||
|
||||
.data 0x811AEAE8
|
||||
.data 4
|
||||
.binary 00000d20
|
||||
|
||||
.data 0x811AEB0C
|
||||
.data 4
|
||||
.binary 00000d98
|
||||
|
||||
.data 0x811AEB30
|
||||
.data 4
|
||||
.binary 00000a78
|
||||
|
||||
.data 0x811AEB54
|
||||
.data 4
|
||||
.binary 00000d5c
|
||||
|
||||
.data 0x811AEB78
|
||||
.data 4
|
||||
.binary 00000ec4
|
||||
|
||||
.data 0x811AEB9C
|
||||
.data 4
|
||||
.binary 00000f78
|
||||
|
||||
.data 0x811AEBC0
|
||||
.data 4
|
||||
.binary 00000b90
|
||||
|
||||
.data 0x811AEBE4
|
||||
.data 4
|
||||
.binary 00000e10
|
||||
|
||||
.data 0x811AEC08
|
||||
.data 4
|
||||
.binary 00000bf4
|
||||
|
||||
.data 0x811AEC2C
|
||||
.data 4
|
||||
.binary 00000c30
|
||||
|
||||
.data 0x811AEC50
|
||||
.data 4
|
||||
.binary 00000c6c
|
||||
|
||||
.data 0
|
||||
.data 0
|
||||
@@ -0,0 +1,974 @@
|
||||
.meta name="PSO Peeps EP2 5x EXP"
|
||||
.meta description="Sets EP2 enemy EXP\nto 5x for GC crossplay"
|
||||
|
||||
entry_ptr:
|
||||
reloc0:
|
||||
.offsetof start
|
||||
|
||||
start:
|
||||
.include WriteCodeBlocksGC
|
||||
|
||||
# PSO Peeps GC Plus USA / 3OE1
|
||||
# Source table: BattleParamEntry_lab_on.dat
|
||||
# Active online battle-param table loaded at 0x811AB7C0
|
||||
# EXP field offset within each 0x24-byte row is +0x1C
|
||||
# Generated from clean BattleParamEntry_lab_on.dat; multiplier=5x
|
||||
|
||||
.data 0x811AB7DC
|
||||
.data 4
|
||||
.binary 00000005
|
||||
|
||||
.data 0x811AB800
|
||||
.data 4
|
||||
.binary 0000001e
|
||||
|
||||
.data 0x811AB824
|
||||
.data 4
|
||||
.binary 00000032
|
||||
|
||||
.data 0x811AB848
|
||||
.data 4
|
||||
.binary 0000003c
|
||||
|
||||
.data 0x811AB86C
|
||||
.data 4
|
||||
.binary 00000032
|
||||
|
||||
.data 0x811AB890
|
||||
.data 4
|
||||
.binary 000002ee
|
||||
|
||||
.data 0x811AB8B4
|
||||
.data 4
|
||||
.binary 0000005f
|
||||
|
||||
.data 0x811AB8D8
|
||||
.data 4
|
||||
.binary 00000032
|
||||
|
||||
.data 0x811AB8FC
|
||||
.data 4
|
||||
.binary 00000050
|
||||
|
||||
.data 0x811AB920
|
||||
.data 4
|
||||
.binary 00000055
|
||||
|
||||
.data 0x811AB944
|
||||
.data 4
|
||||
.binary 00000055
|
||||
|
||||
.data 0x811AB968
|
||||
.data 4
|
||||
.binary 0000000a
|
||||
|
||||
.data 0x811AB98C
|
||||
.data 4
|
||||
.binary 0000000a
|
||||
|
||||
.data 0x811AB9B0
|
||||
.data 4
|
||||
.binary 000000c8
|
||||
|
||||
.data 0x811AB9D4
|
||||
.data 4
|
||||
.binary 0000004b
|
||||
|
||||
.data 0x811AB9F8
|
||||
.data 4
|
||||
.binary 00000960
|
||||
|
||||
.data 0x811ABA1C
|
||||
.data 4
|
||||
.binary 00000032
|
||||
|
||||
.data 0x811ABA40
|
||||
.data 4
|
||||
.binary 0000000f
|
||||
|
||||
.data 0x811ABA64
|
||||
.data 4
|
||||
.binary 0000128e
|
||||
|
||||
.data 0x811ABA88
|
||||
.data 4
|
||||
.binary 00000069
|
||||
|
||||
.data 0x811ABB3C
|
||||
.data 4
|
||||
.binary 00000014
|
||||
|
||||
.data 0x811ABB60
|
||||
.data 4
|
||||
.binary 00000500
|
||||
|
||||
.data 0x811ABB84
|
||||
.data 4
|
||||
.binary 000000ff
|
||||
|
||||
.data 0x811ABBA8
|
||||
.data 4
|
||||
.binary 0000000a
|
||||
|
||||
.data 0x811ABBCC
|
||||
.data 4
|
||||
.binary 00000037
|
||||
|
||||
.data 0x811ABBF0
|
||||
.data 4
|
||||
.binary 0000005f
|
||||
|
||||
.data 0x811ABC14
|
||||
.data 4
|
||||
.binary 00001d4c
|
||||
|
||||
.data 0x811ABCC8
|
||||
.data 4
|
||||
.binary 000002a8
|
||||
|
||||
.data 0x811ABD10
|
||||
.data 4
|
||||
.binary 000000f5
|
||||
|
||||
.data 0x811ABD34
|
||||
.data 4
|
||||
.binary 000000e1
|
||||
|
||||
.data 0x811ABE0C
|
||||
.data 4
|
||||
.binary 00004074
|
||||
|
||||
.data 0x811ABE9C
|
||||
.data 4
|
||||
.binary 00000091
|
||||
|
||||
.data 0x811ABEC0
|
||||
.data 4
|
||||
.binary 0000000f
|
||||
|
||||
.data 0x811ABEE4
|
||||
.data 4
|
||||
.binary 00000023
|
||||
|
||||
.data 0x811ABF08
|
||||
.data 4
|
||||
.binary 00000028
|
||||
|
||||
.data 0x811AC004
|
||||
.data 4
|
||||
.binary 000000eb
|
||||
|
||||
.data 0x811AC028
|
||||
.data 4
|
||||
.binary 0000003c
|
||||
|
||||
.data 0x811AC04C
|
||||
.data 4
|
||||
.binary 0000004b
|
||||
|
||||
.data 0x811AC070
|
||||
.data 4
|
||||
.binary 00000104
|
||||
|
||||
.data 0x811AC0DC
|
||||
.data 4
|
||||
.binary 00000096
|
||||
|
||||
.data 0x811AC100
|
||||
.data 4
|
||||
.binary 0000003c
|
||||
|
||||
.data 0x811AC124
|
||||
.data 4
|
||||
.binary 0000000a
|
||||
|
||||
.data 0x811AC148
|
||||
.data 4
|
||||
.binary 000000a0
|
||||
|
||||
.data 0x811AC16C
|
||||
.data 4
|
||||
.binary 000000c8
|
||||
|
||||
.data 0x811AC190
|
||||
.data 4
|
||||
.binary 000000fa
|
||||
|
||||
.data 0x811AC1B4
|
||||
.data 4
|
||||
.binary 00000113
|
||||
|
||||
.data 0x811AC220
|
||||
.data 4
|
||||
.binary 00000032
|
||||
|
||||
.data 0x811AC244
|
||||
.data 4
|
||||
.binary 000001f4
|
||||
|
||||
.data 0x811AC268
|
||||
.data 4
|
||||
.binary 00000037
|
||||
|
||||
.data 0x811AC28C
|
||||
.data 4
|
||||
.binary 00000041
|
||||
|
||||
.data 0x811AC2B0
|
||||
.data 4
|
||||
.binary 00000023
|
||||
|
||||
.data 0x811AC2D4
|
||||
.data 4
|
||||
.binary 0000003c
|
||||
|
||||
.data 0x811AC2F8
|
||||
.data 4
|
||||
.binary 0000005a
|
||||
|
||||
.data 0x811AC31C
|
||||
.data 4
|
||||
.binary 00000069
|
||||
|
||||
.data 0x811AC340
|
||||
.data 4
|
||||
.binary 00000046
|
||||
|
||||
.data 0x811AC364
|
||||
.data 4
|
||||
.binary 0000004b
|
||||
|
||||
.data 0x811AC388
|
||||
.data 4
|
||||
.binary 0000001e
|
||||
|
||||
.data 0x811AC3AC
|
||||
.data 4
|
||||
.binary 00000023
|
||||
|
||||
.data 0x811AC3D0
|
||||
.data 4
|
||||
.binary 00000028
|
||||
|
||||
.data 0x811AC55C
|
||||
.data 4
|
||||
.binary 000000b9
|
||||
|
||||
.data 0x811AC580
|
||||
.data 4
|
||||
.binary 000000e1
|
||||
|
||||
.data 0x811AC5A4
|
||||
.data 4
|
||||
.binary 00000104
|
||||
|
||||
.data 0x811AC5C8
|
||||
.data 4
|
||||
.binary 00000113
|
||||
|
||||
.data 0x811AC5EC
|
||||
.data 4
|
||||
.binary 00000104
|
||||
|
||||
.data 0x811AC610
|
||||
.data 4
|
||||
.binary 00000587
|
||||
|
||||
.data 0x811AC634
|
||||
.data 4
|
||||
.binary 0000014a
|
||||
|
||||
.data 0x811AC658
|
||||
.data 4
|
||||
.binary 00000104
|
||||
|
||||
.data 0x811AC67C
|
||||
.data 4
|
||||
.binary 00000122
|
||||
|
||||
.data 0x811AC6A0
|
||||
.data 4
|
||||
.binary 0000012c
|
||||
|
||||
.data 0x811AC6C4
|
||||
.data 4
|
||||
.binary 0000013b
|
||||
|
||||
.data 0x811AC6E8
|
||||
.data 4
|
||||
.binary 000000be
|
||||
|
||||
.data 0x811AC70C
|
||||
.data 4
|
||||
.binary 000000be
|
||||
|
||||
.data 0x811AC730
|
||||
.data 4
|
||||
.binary 000001f9
|
||||
|
||||
.data 0x811AC754
|
||||
.data 4
|
||||
.binary 0000012c
|
||||
|
||||
.data 0x811AC778
|
||||
.data 4
|
||||
.binary 00003e80
|
||||
|
||||
.data 0x811AC79C
|
||||
.data 4
|
||||
.binary 0000007d
|
||||
|
||||
.data 0x811AC7C0
|
||||
.data 4
|
||||
.binary 00000014
|
||||
|
||||
.data 0x811AC7E4
|
||||
.data 4
|
||||
.binary 00004c2c
|
||||
|
||||
.data 0x811AC808
|
||||
.data 4
|
||||
.binary 0000014a
|
||||
|
||||
.data 0x811AC8BC
|
||||
.data 4
|
||||
.binary 000000d2
|
||||
|
||||
.data 0x811AC8E0
|
||||
.data 4
|
||||
.binary 00000a00
|
||||
|
||||
.data 0x811AC904
|
||||
.data 4
|
||||
.binary 00000253
|
||||
|
||||
.data 0x811AC928
|
||||
.data 4
|
||||
.binary 00000032
|
||||
|
||||
.data 0x811AC94C
|
||||
.data 4
|
||||
.binary 00000109
|
||||
|
||||
.data 0x811AC970
|
||||
.data 4
|
||||
.binary 0000014a
|
||||
|
||||
.data 0x811AC994
|
||||
.data 4
|
||||
.binary 000057e4
|
||||
|
||||
.data 0x811ACA48
|
||||
.data 4
|
||||
.binary 0000050f
|
||||
|
||||
.data 0x811ACA90
|
||||
.data 4
|
||||
.binary 00000244
|
||||
|
||||
.data 0x811ACAB4
|
||||
.data 4
|
||||
.binary 00000221
|
||||
|
||||
.data 0x811ACB8C
|
||||
.data 4
|
||||
.binary 00009664
|
||||
|
||||
.data 0x811ACC1C
|
||||
.data 4
|
||||
.binary 0000019f
|
||||
|
||||
.data 0x811ACC40
|
||||
.data 4
|
||||
.binary 000000eb
|
||||
|
||||
.data 0x811ACC64
|
||||
.data 4
|
||||
.binary 000000eb
|
||||
|
||||
.data 0x811ACC88
|
||||
.data 4
|
||||
.binary 000000f0
|
||||
|
||||
.data 0x811ACD84
|
||||
.data 4
|
||||
.binary 00000235
|
||||
|
||||
.data 0x811ACDA8
|
||||
.data 4
|
||||
.binary 00000113
|
||||
|
||||
.data 0x811ACDCC
|
||||
.data 4
|
||||
.binary 0000012c
|
||||
|
||||
.data 0x811ACDF0
|
||||
.data 4
|
||||
.binary 0000025d
|
||||
|
||||
.data 0x811ACE5C
|
||||
.data 4
|
||||
.binary 000001a9
|
||||
|
||||
.data 0x811ACE80
|
||||
.data 4
|
||||
.binary 00000113
|
||||
|
||||
.data 0x811ACEA4
|
||||
.data 4
|
||||
.binary 000000e1
|
||||
|
||||
.data 0x811ACEC8
|
||||
.data 4
|
||||
.binary 000001b8
|
||||
|
||||
.data 0x811ACEEC
|
||||
.data 4
|
||||
.binary 000001f9
|
||||
|
||||
.data 0x811ACF10
|
||||
.data 4
|
||||
.binary 0000024e
|
||||
|
||||
.data 0x811ACF34
|
||||
.data 4
|
||||
.binary 00000276
|
||||
|
||||
.data 0x811ACF7C
|
||||
.data 4
|
||||
.binary 00000019
|
||||
|
||||
.data 0x811ACFA0
|
||||
.data 4
|
||||
.binary 00000104
|
||||
|
||||
.data 0x811ACFC4
|
||||
.data 4
|
||||
.binary 000003e8
|
||||
|
||||
.data 0x811ACFE8
|
||||
.data 4
|
||||
.binary 00000109
|
||||
|
||||
.data 0x811AD00C
|
||||
.data 4
|
||||
.binary 00000118
|
||||
|
||||
.data 0x811AD030
|
||||
.data 4
|
||||
.binary 000000e1
|
||||
|
||||
.data 0x811AD054
|
||||
.data 4
|
||||
.binary 00000113
|
||||
|
||||
.data 0x811AD078
|
||||
.data 4
|
||||
.binary 00000145
|
||||
|
||||
.data 0x811AD09C
|
||||
.data 4
|
||||
.binary 0000015e
|
||||
|
||||
.data 0x811AD0C0
|
||||
.data 4
|
||||
.binary 00000113
|
||||
|
||||
.data 0x811AD0E4
|
||||
.data 4
|
||||
.binary 0000012c
|
||||
|
||||
.data 0x811AD108
|
||||
.data 4
|
||||
.binary 000000e1
|
||||
|
||||
.data 0x811AD12C
|
||||
.data 4
|
||||
.binary 000000eb
|
||||
|
||||
.data 0x811AD150
|
||||
.data 4
|
||||
.binary 000000f0
|
||||
|
||||
.data 0x811AD2DC
|
||||
.data 4
|
||||
.binary 000001b3
|
||||
|
||||
.data 0x811AD300
|
||||
.data 4
|
||||
.binary 000001e5
|
||||
|
||||
.data 0x811AD324
|
||||
.data 4
|
||||
.binary 0000020d
|
||||
|
||||
.data 0x811AD348
|
||||
.data 4
|
||||
.binary 00000221
|
||||
|
||||
.data 0x811AD36C
|
||||
.data 4
|
||||
.binary 0000020d
|
||||
|
||||
.data 0x811AD390
|
||||
.data 4
|
||||
.binary 00000785
|
||||
|
||||
.data 0x811AD3B4
|
||||
.data 4
|
||||
.binary 0000023f
|
||||
|
||||
.data 0x811AD3D8
|
||||
.data 4
|
||||
.binary 0000020d
|
||||
|
||||
.data 0x811AD3FC
|
||||
.data 4
|
||||
.binary 00000230
|
||||
|
||||
.data 0x811AD420
|
||||
.data 4
|
||||
.binary 0000023a
|
||||
|
||||
.data 0x811AD444
|
||||
.data 4
|
||||
.binary 00000253
|
||||
|
||||
.data 0x811AD468
|
||||
.data 4
|
||||
.binary 000001bd
|
||||
|
||||
.data 0x811AD48C
|
||||
.data 4
|
||||
.binary 000001bd
|
||||
|
||||
.data 0x811AD4B0
|
||||
.data 4
|
||||
.binary 00000339
|
||||
|
||||
.data 0x811AD4D4
|
||||
.data 4
|
||||
.binary 0000023f
|
||||
|
||||
.data 0x811AD4F8
|
||||
.data 4
|
||||
.binary 0000abe0
|
||||
|
||||
.data 0x811AD51C
|
||||
.data 4
|
||||
.binary 0000012c
|
||||
|
||||
.data 0x811AD540
|
||||
.data 4
|
||||
.binary 0000012c
|
||||
|
||||
.data 0x811AD564
|
||||
.data 4
|
||||
.binary 0000c350
|
||||
|
||||
.data 0x811AD588
|
||||
.data 4
|
||||
.binary 00000267
|
||||
|
||||
.data 0x811AD63C
|
||||
.data 4
|
||||
.binary 000001d1
|
||||
|
||||
.data 0x811AD660
|
||||
.data 4
|
||||
.binary 00000ec4
|
||||
|
||||
.data 0x811AD684
|
||||
.data 4
|
||||
.binary 000003a7
|
||||
|
||||
.data 0x811AD6A8
|
||||
.data 4
|
||||
.binary 00000096
|
||||
|
||||
.data 0x811AD6CC
|
||||
.data 4
|
||||
.binary 00000217
|
||||
|
||||
.data 0x811AD6F0
|
||||
.data 4
|
||||
.binary 00000267
|
||||
|
||||
.data 0x811AD714
|
||||
.data 4
|
||||
.binary 0000f424
|
||||
|
||||
.data 0x811AD7C8
|
||||
.data 4
|
||||
.binary 000006f9
|
||||
|
||||
.data 0x811AD810
|
||||
.data 4
|
||||
.binary 00000393
|
||||
|
||||
.data 0x811AD834
|
||||
.data 4
|
||||
.binary 0000036b
|
||||
|
||||
.data 0x811AD90C
|
||||
.data 4
|
||||
.binary 000124f8
|
||||
|
||||
.data 0x811AD99C
|
||||
.data 4
|
||||
.binary 000002cb
|
||||
|
||||
.data 0x811AD9C0
|
||||
.data 4
|
||||
.binary 000001c7
|
||||
|
||||
.data 0x811AD9E4
|
||||
.data 4
|
||||
.binary 000001ef
|
||||
|
||||
.data 0x811ADA08
|
||||
.data 4
|
||||
.binary 000001f9
|
||||
|
||||
.data 0x811ADB04
|
||||
.data 4
|
||||
.binary 0000037f
|
||||
|
||||
.data 0x811ADB28
|
||||
.data 4
|
||||
.binary 00000221
|
||||
|
||||
.data 0x811ADB4C
|
||||
.data 4
|
||||
.binary 0000023f
|
||||
|
||||
.data 0x811ADB70
|
||||
.data 4
|
||||
.binary 000003b1
|
||||
|
||||
.data 0x811ADBDC
|
||||
.data 4
|
||||
.binary 000002d5
|
||||
|
||||
.data 0x811ADC00
|
||||
.data 4
|
||||
.binary 00000221
|
||||
|
||||
.data 0x811ADC24
|
||||
.data 4
|
||||
.binary 000001db
|
||||
|
||||
.data 0x811ADC48
|
||||
.data 4
|
||||
.binary 000002e9
|
||||
|
||||
.data 0x811ADC6C
|
||||
.data 4
|
||||
.binary 00000339
|
||||
|
||||
.data 0x811ADC90
|
||||
.data 4
|
||||
.binary 0000039d
|
||||
|
||||
.data 0x811ADCB4
|
||||
.data 4
|
||||
.binary 000003cf
|
||||
|
||||
.data 0x811ADCFC
|
||||
.data 4
|
||||
.binary 00000019
|
||||
|
||||
.data 0x811ADD20
|
||||
.data 4
|
||||
.binary 0000020d
|
||||
|
||||
.data 0x811ADD44
|
||||
.data 4
|
||||
.binary 00000591
|
||||
|
||||
.data 0x811ADD68
|
||||
.data 4
|
||||
.binary 00000217
|
||||
|
||||
.data 0x811ADD8C
|
||||
.data 4
|
||||
.binary 0000022b
|
||||
|
||||
.data 0x811ADDB0
|
||||
.data 4
|
||||
.binary 000001d6
|
||||
|
||||
.data 0x811ADDD4
|
||||
.data 4
|
||||
.binary 00000221
|
||||
|
||||
.data 0x811ADDF8
|
||||
.data 4
|
||||
.binary 0000025d
|
||||
|
||||
.data 0x811ADE1C
|
||||
.data 4
|
||||
.binary 0000027b
|
||||
|
||||
.data 0x811ADE40
|
||||
.data 4
|
||||
.binary 0000021c
|
||||
|
||||
.data 0x811ADE64
|
||||
.data 4
|
||||
.binary 0000023f
|
||||
|
||||
.data 0x811ADE88
|
||||
.data 4
|
||||
.binary 000001e5
|
||||
|
||||
.data 0x811ADEAC
|
||||
.data 4
|
||||
.binary 000001ef
|
||||
|
||||
.data 0x811ADED0
|
||||
.data 4
|
||||
.binary 000001f9
|
||||
|
||||
.data 0x811AE05C
|
||||
.data 4
|
||||
.binary 000002ee
|
||||
|
||||
.data 0x811AE080
|
||||
.data 4
|
||||
.binary 000005fa
|
||||
|
||||
.data 0x811AE0A4
|
||||
.data 4
|
||||
.binary 00000672
|
||||
|
||||
.data 0x811AE0C8
|
||||
.data 4
|
||||
.binary 000006ae
|
||||
|
||||
.data 0x811AE0EC
|
||||
.data 4
|
||||
.binary 00000672
|
||||
|
||||
.data 0x811AE110
|
||||
.data 4
|
||||
.binary 000016da
|
||||
|
||||
.data 0x811AE134
|
||||
.data 4
|
||||
.binary 00000708
|
||||
|
||||
.data 0x811AE158
|
||||
.data 4
|
||||
.binary 00000672
|
||||
|
||||
.data 0x811AE17C
|
||||
.data 4
|
||||
.binary 000005f0
|
||||
|
||||
.data 0x811AE1A0
|
||||
.data 4
|
||||
.binary 00000604
|
||||
|
||||
.data 0x811AE1C4
|
||||
.data 4
|
||||
.binary 00000744
|
||||
|
||||
.data 0x811AE1E8
|
||||
.data 4
|
||||
.binary 000001f4
|
||||
|
||||
.data 0x811AE20C
|
||||
.data 4
|
||||
.binary 000001f4
|
||||
|
||||
.data 0x811AE230
|
||||
.data 4
|
||||
.binary 000009f6
|
||||
|
||||
.data 0x811AE254
|
||||
.data 4
|
||||
.binary 00000708
|
||||
|
||||
.data 0x811AE278
|
||||
.data 4
|
||||
.binary 00015f90
|
||||
|
||||
.data 0x811AE29C
|
||||
.data 4
|
||||
.binary 0000012c
|
||||
|
||||
.data 0x811AE2C0
|
||||
.data 4
|
||||
.binary 00000050
|
||||
|
||||
.data 0x811AE2E4
|
||||
.data 4
|
||||
.binary 00019a28
|
||||
|
||||
.data 0x811AE308
|
||||
.data 4
|
||||
.binary 00000780
|
||||
|
||||
.data 0x811AE3BC
|
||||
.data 4
|
||||
.binary 000005be
|
||||
|
||||
.data 0x811AE3E0
|
||||
.data 4
|
||||
.binary 00001400
|
||||
|
||||
.data 0x811AE404
|
||||
.data 4
|
||||
.binary 00000b40
|
||||
|
||||
.data 0x811AE428
|
||||
.data 4
|
||||
.binary 000000fa
|
||||
|
||||
.data 0x811AE44C
|
||||
.data 4
|
||||
.binary 00000690
|
||||
|
||||
.data 0x811AE470
|
||||
.data 4
|
||||
.binary 00000780
|
||||
|
||||
.data 0x811AE494
|
||||
.data 4
|
||||
.binary 0001e26c
|
||||
|
||||
.data 0x811AE548
|
||||
.data 4
|
||||
.binary 00001536
|
||||
|
||||
.data 0x811AE590
|
||||
.data 4
|
||||
.binary 00000b04
|
||||
|
||||
.data 0x811AE5B4
|
||||
.data 4
|
||||
.binary 00000a8c
|
||||
|
||||
.data 0x811AE68C
|
||||
.data 4
|
||||
.binary 00039fbc
|
||||
|
||||
.data 0x811AE71C
|
||||
.data 4
|
||||
.binary 000008ac
|
||||
|
||||
.data 0x811AE740
|
||||
.data 4
|
||||
.binary 00000618
|
||||
|
||||
.data 0x811AE764
|
||||
.data 4
|
||||
.binary 00000618
|
||||
|
||||
.data 0x811AE788
|
||||
.data 4
|
||||
.binary 00000636
|
||||
|
||||
.data 0x811AE884
|
||||
.data 4
|
||||
.binary 000006cc
|
||||
|
||||
.data 0x811AE8A8
|
||||
.data 4
|
||||
.binary 000006ae
|
||||
|
||||
.data 0x811AE8CC
|
||||
.data 4
|
||||
.binary 00000708
|
||||
|
||||
.data 0x811AE8F0
|
||||
.data 4
|
||||
.binary 00000b5e
|
||||
|
||||
.data 0x811AE95C
|
||||
.data 4
|
||||
.binary 000008ca
|
||||
|
||||
.data 0x811AE980
|
||||
.data 4
|
||||
.binary 000006ae
|
||||
|
||||
.data 0x811AE9A4
|
||||
.data 4
|
||||
.binary 000003e8
|
||||
|
||||
.data 0x811AE9C8
|
||||
.data 4
|
||||
.binary 00000906
|
||||
|
||||
.data 0x811AE9EC
|
||||
.data 4
|
||||
.binary 000009f6
|
||||
|
||||
.data 0x811AEA10
|
||||
.data 4
|
||||
.binary 00000b22
|
||||
|
||||
.data 0x811AEA34
|
||||
.data 4
|
||||
.binary 00000bb8
|
||||
|
||||
.data 0x811AEA7C
|
||||
.data 4
|
||||
.binary 00000019
|
||||
|
||||
.data 0x811AEAA0
|
||||
.data 4
|
||||
.binary 00000672
|
||||
|
||||
.data 0x811AEAC4
|
||||
.data 4
|
||||
.binary 000010fe
|
||||
|
||||
.data 0x811AEAE8
|
||||
.data 4
|
||||
.binary 00000690
|
||||
|
||||
.data 0x811AEB0C
|
||||
.data 4
|
||||
.binary 000006cc
|
||||
|
||||
.data 0x811AEB30
|
||||
.data 4
|
||||
.binary 0000053c
|
||||
|
||||
.data 0x811AEB54
|
||||
.data 4
|
||||
.binary 000006ae
|
||||
|
||||
.data 0x811AEB78
|
||||
.data 4
|
||||
.binary 00000762
|
||||
|
||||
.data 0x811AEB9C
|
||||
.data 4
|
||||
.binary 000007bc
|
||||
|
||||
.data 0x811AEBC0
|
||||
.data 4
|
||||
.binary 000005c8
|
||||
|
||||
.data 0x811AEBE4
|
||||
.data 4
|
||||
.binary 00000708
|
||||
|
||||
.data 0x811AEC08
|
||||
.data 4
|
||||
.binary 000005fa
|
||||
|
||||
.data 0x811AEC2C
|
||||
.data 4
|
||||
.binary 00000618
|
||||
|
||||
.data 0x811AEC50
|
||||
.data 4
|
||||
.binary 00000636
|
||||
|
||||
.data 0
|
||||
.data 0
|
||||
@@ -0,0 +1,44 @@
|
||||
# (uint16_t entity_id @ eax) -> TObjectV00b421c0* @ eax
|
||||
# Preserves all registers except eax
|
||||
get_enemy_entity:
|
||||
push esi
|
||||
push edi
|
||||
push edx
|
||||
push ecx
|
||||
xor edx, edx
|
||||
xchg edx, eax
|
||||
cmp edx, 0x1000
|
||||
jl done
|
||||
cmp edx, 0x4000
|
||||
jge done
|
||||
|
||||
mov esi, [0x00AABCE8] # bs_low = next_player_entity_index
|
||||
mov edi, [0x00AABCE4]
|
||||
lea edi, [edi + esi - 1] # bs_high = next_player_entity_index + next_enemy_entity_index - 1
|
||||
bs_again:
|
||||
cmp esi, edi
|
||||
jge bs_done
|
||||
lea ecx, [esi + edi]
|
||||
shr ecx, 1
|
||||
mov eax, [ecx * 4 + 0x00AAB2A0] # all_entities[ecx]
|
||||
cmp [eax + 0x1C], dx
|
||||
jge bs_not_less
|
||||
lea esi, [ecx + 1]
|
||||
jmp bs_again
|
||||
bs_not_less:
|
||||
mov edi, ecx
|
||||
jmp bs_again
|
||||
bs_done:
|
||||
|
||||
mov eax, [esi * 4 + 0x00AAB2A0] # all_entities[bs_low]
|
||||
test eax, eax
|
||||
je done
|
||||
xor ecx, ecx
|
||||
cmp [eax + 0x1C], dx
|
||||
cmovne eax, ecx
|
||||
|
||||
done:
|
||||
pop ecx
|
||||
pop edx
|
||||
pop edi
|
||||
pop esi
|
||||
@@ -0,0 +1,42 @@
|
||||
# This file defines the following function:
|
||||
# write_address_of_code(
|
||||
# const void* patch_code,
|
||||
# size_t patch_code_size,
|
||||
# void** ptr_addr);
|
||||
# This function allocates memory for patch_code, copies patch_code to that
|
||||
# memory, then writes the address of the allocated code at the specified
|
||||
# pointer. The allocated memory is never freed.
|
||||
# This function pops its arguments off the stack before returning.
|
||||
|
||||
write_call_to_code:
|
||||
# [esp + 0x04] = code ptr
|
||||
# [esp + 0x08] = code size
|
||||
# [esp + 0x0C] = ptr addr
|
||||
|
||||
# Allocate memory for the copied code
|
||||
mov ecx, [0x00AA8F84]
|
||||
push dword [esp + 0x08]
|
||||
mov eax, 0x007A984C
|
||||
call eax # malloc7
|
||||
test eax, eax
|
||||
je done
|
||||
|
||||
# Copy the code to the newly-allocated memory
|
||||
# eax = dest pointer (from malloc7 call above)
|
||||
mov edx, [esp + 0x04] # edx = source pointer
|
||||
mov ecx, [esp + 0x08] # ecx = source size
|
||||
push ebx
|
||||
memcpy_again:
|
||||
dec ecx
|
||||
mov bl, [edx + ecx] # Copy one byte from source to dest
|
||||
mov [eax + ecx], bl
|
||||
test ecx, ecx
|
||||
jne memcpy_again
|
||||
pop ebx
|
||||
|
||||
# Write the address
|
||||
mov ecx, [esp + 0x0C]
|
||||
mov [ecx], eax
|
||||
|
||||
done:
|
||||
ret 0x0C
|
||||
@@ -0,0 +1,76 @@
|
||||
# This file defines the following function:
|
||||
# write_call_to_code(
|
||||
# const void* patch_code,
|
||||
# size_t patch_code_size,
|
||||
# void* call_opcode_address,
|
||||
# ssize_t call_opcode_bytes);
|
||||
# This function allocates memory for patch_code, copies patch_code to that
|
||||
# memory, then writes a call or jmp opcode to call_opcode_address that calls
|
||||
# the code in the allocated memory region. The allocated memory is never freed.
|
||||
# call_opcode_bytes specifies how many bytes at the callsite should be
|
||||
# overwritten. This value must be at least 5; the first 5 bytes are overwritten
|
||||
# with the call/jmp opcode itself; the rest are overwritten with nop opcodes.
|
||||
# If call_opcode_bytes is positive, a call opcode is written; if it's negative,
|
||||
# a jmp opcode is written.
|
||||
# This function pops its arguments off the stack before returning.
|
||||
|
||||
write_call_to_code:
|
||||
# [esp + 0x04] = code ptr
|
||||
# [esp + 0x08] = code size
|
||||
# [esp + 0x0C] = jump callsite
|
||||
# [esp + 0x10] = callsite size (if zero, write the address instead of a call)
|
||||
|
||||
# Allocate memory for the copied code
|
||||
mov ecx, [0x00AA8F84]
|
||||
push dword [esp + 0x08]
|
||||
mov eax, 0x007A984C
|
||||
call eax # malloc7
|
||||
test eax, eax
|
||||
je done
|
||||
|
||||
# Copy the code to the newly-allocated memory
|
||||
# eax = dest pointer (from malloc7 call above)
|
||||
mov edx, [esp + 0x04] # edx = source pointer
|
||||
mov ecx, [esp + 0x08] # ecx = source size
|
||||
push ebx
|
||||
memcpy_again:
|
||||
dec ecx
|
||||
mov bl, [edx + ecx] # Copy one byte from source to dest
|
||||
mov [eax + ecx], bl
|
||||
test ecx, ecx
|
||||
jne memcpy_again
|
||||
pop ebx
|
||||
|
||||
mov edx, [esp + 0x0C] # edx = jump callsite
|
||||
|
||||
# If the callsite size is zero, just write the address directly
|
||||
cmp dword [esp + 0x10], 0
|
||||
jne write_call_or_jmp
|
||||
mov [edx], eax
|
||||
jmp done
|
||||
|
||||
# Write the call or jmp opcode
|
||||
write_call_or_jmp:
|
||||
lea ecx, [eax - 5]
|
||||
sub ecx, edx # ecx = (dest code addr) - (jump callsite) - 5
|
||||
cmp dword [esp + 0x10], 0
|
||||
setl al
|
||||
or al, 0xE8
|
||||
mov [edx], al # Write E8 (call), or E9 (jmp) if size was negative
|
||||
mov [edx + 1], ecx # Write delta
|
||||
|
||||
# Write as many nops after the call opcode as necessary
|
||||
mov ecx, 5
|
||||
mov eax, [esp + 0x10]
|
||||
cmp eax, 0
|
||||
jge write_nop_again
|
||||
neg eax
|
||||
write_nop_again:
|
||||
cmp ecx, eax
|
||||
jge done
|
||||
mov byte [edx + ecx], 0x90
|
||||
inc ecx
|
||||
jmp write_nop_again
|
||||
|
||||
done:
|
||||
ret 0x10
|
||||
@@ -0,0 +1,83 @@
|
||||
# This file defines the following function:
|
||||
# void [/std] write_call_to_code(
|
||||
# const void* patch_code @ [esp + 0x04],
|
||||
# size_t patch_code_size @ [esp + 0x08],
|
||||
# size_t call_count @ [esp + 0x0C],
|
||||
# void* call_opcode_address @ [esp + 0x10],
|
||||
# ssize_t call_opcode_bytes @ [esp + 0x14],
|
||||
# ...);
|
||||
# This function allocates memory for patch_code, copies patch_code to that
|
||||
# memory, then writes a call or jmp opcode to call_opcode_address that calls
|
||||
# the code in the allocated memory region. The allocated memory is never freed.
|
||||
# call_opcode_bytes specifies how many bytes at the callsite should be
|
||||
# overwritten. This value must be at least 5; the first 5 bytes are overwritten
|
||||
# with the call/jmp opcode itself; the rest are overwritten with nop opcodes.
|
||||
# This function pops its arguments off the stack before returning (including
|
||||
# all the varargs).
|
||||
|
||||
write_call_to_code:
|
||||
# [esp + 0x04] = code ptr
|
||||
# [esp + 0x08] = code size
|
||||
# [esp + 0x0C] = callsite count
|
||||
# [esp + 0x10] = callsite address
|
||||
# [esp + 0x14] = callsite size
|
||||
# ... (further callsite address/size pairs)
|
||||
|
||||
# Allocate memory for the copied code
|
||||
mov ecx, [0x00AA8F84]
|
||||
push dword [esp + 0x08]
|
||||
mov eax, 0x007A984C
|
||||
call eax # malloc7
|
||||
test eax, eax
|
||||
je done
|
||||
|
||||
# Copy the code to the newly-allocated memory
|
||||
# eax = dest pointer (from malloc7 call above)
|
||||
mov edx, [esp + 0x04] # edx = source pointer
|
||||
mov ecx, [esp + 0x08] # ecx = source size
|
||||
push ebx
|
||||
memcpy_again:
|
||||
dec ecx
|
||||
mov bl, [edx + ecx] # Copy one byte from source to dest
|
||||
mov [eax + ecx], bl
|
||||
test ecx, ecx
|
||||
jne memcpy_again
|
||||
pop ebx
|
||||
|
||||
# Write the call opcodes
|
||||
xchg ebx, [esp + 0x0C] # Save ebx; get callsite count
|
||||
mov [esp - 0x08], esi
|
||||
mov [esp - 0x0C], eax
|
||||
mov esi, 0x10 # Stack offset of first callsite pair
|
||||
|
||||
next_callsite:
|
||||
mov edx, [esp + esi] # edx = jump callsite
|
||||
lea ecx, [eax - 5]
|
||||
sub ecx, edx # ecx = (dest code addr) - (jump callsite) - 5
|
||||
mov byte [edx], 0xE8
|
||||
mov [edx + 1], ecx # Write E8 (call) followed by delta
|
||||
|
||||
# Write as many nops after the call opcode as necessary
|
||||
mov ecx, 5
|
||||
mov eax, [esp + esi + 4]
|
||||
write_nop_again:
|
||||
cmp ecx, eax
|
||||
jge this_callsite_done
|
||||
mov byte [edx + ecx], 0x90
|
||||
inc ecx
|
||||
jmp write_nop_again
|
||||
|
||||
this_callsite_done:
|
||||
mov eax, [esp - 0x0C]
|
||||
add esi, 8
|
||||
dec ebx
|
||||
jnz next_callsite
|
||||
|
||||
mov ecx, esi
|
||||
mov ebx, [esp + 0x0C]
|
||||
mov esi, [esp - 0x08]
|
||||
|
||||
done:
|
||||
mov eax, [esp]
|
||||
add esp, ecx
|
||||
jmp eax
|
||||
@@ -576,14 +576,14 @@
|
||||
],
|
||||
|
||||
// BB bank size. If you change either of these values, you must also add "BankSize" to BBRequiredPatches, and change
|
||||
// the patch contents in system/client-functions/BlueBurstExclusive/BankSize.59NL.patch.s to reflect the counts here.
|
||||
// the patch contents in system/client-functions/BlueBurstExclusive/BankSize.5___.patch.s to reflect the counts here.
|
||||
"BBMaxBankItems": 200,
|
||||
"BBMaxBankMeseta": 999999,
|
||||
|
||||
// Item stack limits. Note that changing these does not affect the client's behavior automatically - this only exists
|
||||
// to allow the server to understand the behavior of clients that are already patched with different stack limits.
|
||||
// If you want to use an unpatched BB client but still have custom stack limits, you can use the StackLimits runtime
|
||||
// patch by editing system/client-functions/BlueBurstExclusive/StackLimits.59NL.patch.s to match the BB stack limits
|
||||
// patch by editing system/client-functions/BlueBurstExclusive/StackLimits.5___.patch.s to match the BB stack limits
|
||||
// and adding "StackLimits" to the BBRequiredPatches list.
|
||||
// It's important that all players in the same game have the same stack limits, both on the client and server! So, if
|
||||
// you change the BB stack limits, you must either prevent BB from playing with other versions (see
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
ItemCT-pc-v2.afs
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1 +0,0 @@
|
||||
ItemPMT-pc-v2.prs
|
||||
@@ -1 +0,0 @@
|
||||
ItemPMT-gc-v3.prs
|
||||
@@ -1 +0,0 @@
|
||||
ItemPMT-gc-v3.prs
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user