A secure and fast patcher for managing Perfect World game client updates. Fully compatible with the original protocol, but with a modern architecture and type safety.
- Possibilities
- Architecture
- Requirements
- Installation
- Configuration
- Command CLI
- Examples of use
- Docker
- Safety
- Troubleshooting
- Migrating from Java
| Feature | Description |
|---|---|
| RSA Signing | Key generation (1024-bit), manifest signing via MD5withRSA |
| MySQL/PostgreSQL | Abstract database layer, parameterized queries, and migrations |
| File Packing | [4-byte LE size][deflate(data)] format for client compatibility |
| Incremental Patches | Automated generation of v-N.inc for fast updates |
| Docker-ready | Minimal footprint images and health checks |
- Integrate CPW into the Web Admin Panel
- Upgrade RSA signature size to 2048-bit
- Migrate from MD5withRSA to SHA-256withRSA
- New modern launcher
lib/
โโโ app/ # Entry point and dispatching
โ โโโ cli_runner.dart # Argument parsing and command execution
โ โโโ command_info.dart # Command metadata
โ โโโ command_registry.dart # Command registry + menu output
โ
โโโ config/ # Configuration
โ โโโ config.dart # Exports
โ โโโ patcher_config.dart # Typed immutable model
โ โโโ config_loader.dart # Loading .conf + env overrides
โ โโโ config_parser.dart # Parsing .properties
โ
โโโ core/ # Infrastructure and shared services
โ โโโ crypto/ # Cryptography and encoding
โ โ โโโ crypto.dart # Crypto module exports
โ โ โโโ exceptions.dart # Typed encryption errors
โ โ โโโ file_key_storage.dart # Implementation keys.json
โ โ โโโ key_storage_interface.dart # Key storage abstraction
โ โ โโโ models/
โ โ โ โโโ rsa_key_pair.dart # Key pair model (Public/Private)
โ โ โโโ utils/
โ โ โโโ base64_path_encoder.dart # Path encoding
โ โ โโโ rsa_utils.dart # RSA helpers
โ โ
โ โโโ database/ # Database management
โ โ โโโ database.dart # DB module exports
โ โ โโโ database_interface.dart # IDatabase (abstraction)
โ โ โโโ db_service.dart # DB business logic
โ โ โโโ exceptions.dart # Typed DB errors
โ โ โโโ utils/
โ โ โ โโโ sql_script_parser.dart # SQL script helper
โ โ โโโ adapters/
โ โ โโโ mysql_adapter.dart # MySQL implementation
โ โ โโโ postgres_adapter.dart # PostgreSQL implementation
โ โ
โ โโโ logger/
โ โ โโโ logger_service.dart # Logging: console + files
โ โ
โ โโโ utils/
โ โโโ ansi_colors.dart # Terminal colors
โ โโโ safe_path.dart # Paths safety
โ โโโ utilities.dart # Shared project utilities
โ
โโโ di/ # Dependency Injection
โ โโโ service_locator.dart # get_it locator configuration
โ
โโโ features/ # Business features (isolated modules)
โโโ revisions/ # Version and revision management
โ โโโ manifest_service.dart # Generation of files.md5 + v-N.inc
โ โโโ packer_service.dart # Packing [size][deflate]
โ โโโ revisions.dart # Revision module exports
โ โโโ revision_service.dart # Revision logic: initial, new
โ โโโ commands/
โ โ โโโ initial_command.dart # ./cpw initial
โ โ โโโ listgen_command.dart # ./cpw listgen
โ โ โโโ new_command.dart # ./cpw new
โ โโโ models/
โ โโโ revision_state.dart # Revision state
โ
โโโ security/ # Security and binary patching
โ โโโ binary_patcher_service.dart # Key injection into binaries
โ โโโ rsa_service.dart # Generation + signing
โ โโโ security.dart # Security module exports
โ โโโ commands/
โ โโโ patch_command.dart # ./cpw x [exe]
โ โโโ rsagen_command.dart # ./cpw rsagen
โ
โโโ setup/ # Initialization and deployment
โโโ setup.dart # Initialization module exports
โโโ setup_service.dart # System initialization
โโโ validators.dart # Environment validation
โโโ commands/
โโโ install_command.dart # ./cpw install
- Feature-first: Code is grouped around business capabilities.
- Dependency Inversion: Business logic depends on abstractions, not implementations.
- Single Responsibility: Each class has a single, well-defined purpose.
| Component | Version | Purpose |
|---|---|---|
| Dart SDK | โฅ 3.10.0 | Records, pattern matching, async/await |
| MySQL | โฅ 5.7 | Storing file metadata and revisions |
| Docker | โฅ 24.0 | Optional: for containerization |
# Dart
dart --version # Dart SDK version: 3.10.0 or later
# MySQL
mysql --version # mysql Ver 8.x or 5.7.x
# MariaDB
mariadb --version # mariadb (MySQL) Ver 15.1 Distrib 10.6.25-MariaDBgit clone https://github.com/MrBIOSs/cpw_pw.git
cd cpw_pwdart pub get# For Windows
dart compile exe bin/cpw.dart -o bin/cpw.exe
# For Linux
dart compile exe bin/cpw.dart -o bin/cpw
# verification
./bin/cpwcurl -L -s "https://github.com/MrBIOSs/cpw_pw/releases/latest/download/cpw" -o ./cpwAfter downloading the binary, create or download the files /config/patcher.conf and /config/install_mysql.sql in the same folder.
./
โโโ config/
โ โโโ patcher.conf # Main configuration settings
โ โโโ install_mysql.sql # Database initialization script
โ โโโ install_postgres.sql # Database initialization script
โ โโโ keys.json # RSA keys
โโโ files/
โ โโโ new/ # Input: Source files from developers
โ โ โโโ element/
โ โ โโโ launcher/
โ โ โโโ patcher/
โ โโโ CPW/ # Output: Files ready for distribution
โ โโโ element/
โ โโโ launcher/
โ โโโ patcher/
โโโ log/ # Application logs
โ โโโ console.log
โ โโโ errors.log
โโโ bin/cpw # Executable binary
# DB connection
db-host=localhost
db-port=3306
db-name=pw
db-user=user
db-password=password
# Paths (relative to baseDir)
patch-path=files
patch-new-dir=new
patch-cpw-dir=CPW
# Minimum client versions
min-element-ver=1
min-launcher-ver=1
min-patcher-ver=1
# Flags
remove-input-files=true
add-file-size-to-inc=true# Docker / CI / Production
export DB_HOST=db.prod.internal
export DB_PASSWORD=${VAULT_DB_PASSWORD}
./bin/cpw installSupported Variables:
| Env Variable | Config Key | Description |
|---|---|---|
DB_HOST |
db-host |
Database host |
DB_PORT |
db-port |
Database port |
DB_USER |
db-user |
Database user |
DB_PASSWORD |
db-password |
Database password |
DB_NAME |
db-name |
Database name |
CPW_PATCH_PATH |
patch-path |
Base path for files |
$ ./bin/cpw
Usage:
./cpw install Install updater: database setup, RSA keys generation, paths
./cpw rsagen Regenerate RSA keys
./cpw x [executable] Patch executable with public RSA key
./cpw initial Creates initial (base) revision state
./cpw new Creates the next revision, packs files and generates manifests
./cpw listgen Regenerate files.md5 and incremental patches from current DB state# Interactive installation
./cpw install
# Skip key generation (if already present)
./cpw install --skip-keys
# Skip database initialization (if already configured)
./cpw install --skip-db
# Preview without changes
./cpw install --helpWhat it does:
- Creates the directory structure:
files/new/andfiles/CPW/ - Initializes
versionfiles with the value set inmin-*-ver - (Optional) Creates database tables using
install_*.sql - (Optional) Generates RSA keys and saves them to
keys.json
# Standard generation
./cpw rsagenWhat it does:
- Generates an RSA-1024 key pair
- Saves it to
config/keys.json(atomically, with a backup) - Outputs the public key for copying
Output Format:
Generated Public Key:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# RSA Public Key (copy-paste ready)
# Modulus (hex): a1b2c3...
# Exponent: 65537
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Keys saved to config/keys.json
Tip: Set file permissions to 600 for security: chmod 600 config/keys.json
# Patching an executable file
./cpw x client/Launcher.exe
# With a custom marker (if different from the default)
./cpw x client/Launcher.exe --marker="__CUSTOM_MARKER__"
# A preview of what will be done
./cpw x client/Launcher.exe --helpClient Requirements: The client source code must include a placeholder token of sufficient size.
What it does:
- Locates the token in the binary (via byte pattern search)
- Replaces it with the serialized public key (format: 4 Base64 strings)
- Preserves the original file size (via zero-padding)
- Verifies the write operation
# Standard launch
./cpw initial
# Preview
./cpw initial --helpWhat it does:
- Creates the directory structure:
files/new/{element,launcher,patcher}/ - Creates the
files/CPW/{type}/{type}/directory structure for packaged files - Initializes
versionfiles with the value set inmin-*-ver(defaults to 1) - Synchronizes the state with the database
- Creates
/info/pidwith default value 101
Warning: Delete all *.sw files except version on the original client in the /config/(element, launcher, patcher) folder.
# Standard launch
./cpw new
# Skip manifest generation (packaging + DB only)
./cpw new --skip-manifests
# Forced overwrite (crash recovery)
./cpw new --force
# Preview
./cpw new --helpWhat it does:
- Reads files from
files/new/{type}/(recursively) - Compresses each file using the format:
[4-byte LE size][deflate(data, level=1)] - Renames the files to Base64 (e.g.,
data/config.inibecomesZGF0YQ==/Y29uZmlnLmluaQ==) - Calculates the MD5 checksum of the compressed file
- Writes metadata to the database (via UPSERT)
- Automatically generates manifests (
files.md5,v-N.inc, along with the RSA signature) - Updates the
versionfiles - Removes input files if
remove-input-filesflag is true.
Result:
Revision published successfully!
New revision state:
element: v2
launcher: v1
patcher: v1
Clients can now update to this revision.
Note: Ready-made update files can be obtained from the original Launcher if /patcher/server/updateserver.txt contains a link to an open source with files
# Complete regeneration of all types
./cpw listgen
# Only for specific type
./cpw listgen --type=element
# Preview
./cpw listgen --helpWhat it does:
- Reads the current revision state from the database
- Generates
files.md5:# 2 abc123... data/Y29uZmlnLmluaQ # first entry in the folder def456... data/Y29uZmlnLmlua= # the rest are in the same folder - Signs the manifest using MD5withRSA
- Generates incremental patches (
v-N.inc) with the following prefixes:+- File added within this revision range!- File modified within this range
- Updates the
versionfiles
When to use:
- Accidentally deleted or corrupted
files.md5orv-N.inc - After manually modifying database records
- For auditing purposes: to regenerate manifests from the current database state
- Download CPW from Releases.
- Configure your database and settings in
config/patcher.conf. - Upload the files to any folder on your server.
- Run ./cpw install to set up the DB tables and generate your RSA keys.
- Run ./cpw initial to create the base (initial) revision state and prepare for updates.
- Create a symlink to make the output files downloadable via your web server:
ln -s /path/to/files/CPW /var/www/html/<NAME>- Drop your updated game files into the files/new/directory. For example:
/element/data/tasks.data- For .pck changes: unpack the archive beforehand and put only the specific files you want to add to the update (e.g.
/element/surfaces/<FOLDER_NAME>/<FILE_NAME>).
- In your original client, delete all
*.swfiles exceptversion.swinside the/config/(element, launcher, patcher)folders. Set the version to 1 inside those version.sw files. Also, set 101 in/patcher/server/pid.inisince the default value in CPW is 101. - Copy
Launcher.exeandpatcher.exefrom your client to any working directory on your server. - Patch the client binaries with your generated public key using ./cpw x /path/to/Launcher.exe and ./cpw x /path/to/patcher.exe.
- Copy the patched executables back into your client folder (overwrite the old ones).
- Update the patcher URL in the client's
/patcher/server/updateserver.txtfile to point to your actual URL:
http://<ADDRESS>:<PORT>/<NAME>/
- Run the launcher and test the update.
# 1. System Initialization
./cpw install
# Prompts for database credentials, generates keys, and creates directory structure
# 2. Creating a base revision
./cpw initial
# 3. Preparing Source Files
cp -r ~/game-client/element/* files/new/element/
cp -r ~/game-client/launcher/* files/new/launcher/
# 4. Creating the First Revision
./cpw new
# Packs files, writes metadata to the DB, and generates manifests
# 5. Patching the Client Binaries (Done once during build)
./cpw x client/launcher.exe
./cpw x client/patcher.exe
# 6. Distribution: Files inside files/CPW/ are ready for client download# 1. We put new/modified files
cp new_feature.data files/new/element/data/
# for .pck
cp *.pck.files/ files/new/element/name_pck/
# 2. Create a new revision
./cpw new
# Automatic: packaging, database, revision +1, manifests
# 3. Synchronize with the distribution server
rsync -av files/CPW/ cdn-server:/var/www/patch/# If the disk is full or the manifests are corrupted
# 1. Checking the database status
mysql -u user -p password -e "SELECT MAX(revision) FROM files WHERE type='element';"
# 2. Regenerating manifests from the database
./cpw listgen
# 3. Checking the integrity
./cpw listgen --helpExample using Docker. The following services are considered: pw-db, pw-web, and pw-server. pw-web includes CPW and the web pwadmin.
# docker-compose.yml
services:
pw-db:
image: mariadb:10.6
container_name: pw-db
restart: unless-stopped
env_file: .env
environment:
MARIADB_DATABASE: ${DB_NAME}
MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MARIADB_USER: ${DB_USER}
MARIADB_PASSWORD: ${DB_PASSWORD}
command: [
"--sql-mode=",
"--lower-case-table-names=1",
"--character-set-server=latin1",
"--collation-server=latin1_swedish_ci",
"--innodb-buffer-pool-size=512M",
"--max-connections=500"
]
volumes:
- db_data:/var/lib/mysql
- ./sql:/docker-entrypoint-initdb.d:ro
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 5s
timeout: 5s
retries: 5
start_period: 20s
networks:
- pw-net
pw-web:
build:
context: .
dockerfile: Dockerfile.web
container_name: pw-web
restart: unless-stopped
environment:
PW_SERVER_HOST: pw-server
DB_HOST: pw-db
DB_NAME: ${DB_NAME}
DB_USER: ${DB_USER}
DB_PASSWORD: ${DB_PASSWORD}
volumes:
- ./etc:/etc
- ./patcher:/opt/pw/patcher
- ./patcher/files/CPW:/var/www/html/autopatcher:ro
- ./pwadmin:/var/www/html/pwadmin
ports:
- "80:80"
depends_on:
pw-db:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/"]
interval: 10s
timeout: 5s
retries: 3
networks:
- pw-net
networks:
pw-net:
driver: bridge
volumes:
db_data:# Launch
docker-compose up -d
# Viewing logs
docker-compose logs -f pw-web
# Executing a command in a container
docker-compose exec pw-web /opt/pw/patcher/cpw listgenNginx to transfer finished files to the client.
# Dockerfile
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
nginx ca-certificates curl \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
ARG BASE_URL="https://github.com/MrBIOSs/cpw_pw/releases/latest/download"
RUN curl -L -s "${BASE_URL}/cpw" -o ./cpw \
&& curl -L -s "${BASE_URL}/cpw.sha256" -o ./cpw.sha256 \
&& sha256sum -c cpw.sha256 \
&& chmod +x ./cpw \
&& rm cpw.sha256
COPY config/ ./config/
RUN useradd -m -u 1000 patcher && chown -R patcher:patcher /app
USER patcher
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD ./cpw || exit 1
CMD ["sh", "-c", "nginx -g 'daemon off;' & exec /app/bin/server"]docker build -t pw-web .and
docker exec -it pw-web ./cpw# After generating the keys
chmod 600 config/keys.json
chown patcher:patcher config/keys.json
# For logs
chmod 750 log/
chown -R patcher:patcher log/- Private keys are never logged.
# Vulnerability check
dart pub outdated --mode=null-safety
dart pub deps --style=compact
# Update
dart pub upgrade --major-versions
dart pub get
# Reanalysis
dart analyze
dart testCause: The -----BEGIN PUBLIC KEY----- placeholder token was not found in the binary.
Solution:
- Ensure the token exists in the client's source code.
- Try specifying a custom token:
./cpw x client.exe --marker="__MARKER__"
Cause: Failed to connect to the database.
Diagnostics:
# Access check
mysql -h $DB_HOST -u $DB_USER -p$DB_PASSWORD -e "SELECT 1;"
# Permissions check
mysql -h $DB_HOST -u $DB_USER -p$DB_PASSWORD $DB_NAME -e "SHOW TABLES;"Solution:
- Make sure the database is running and accessible over the network
- Check the connection parameters in
patcher.confor env vars - Make sure the user has
CREATE TABLE,INSERT, andSELECTpermissions
Cause: The serialized public key (216 bytes Base64 + 3 \n = 219 bytes) does not fit into the token.
Cause: The version file was manually modified and does not match the database.
Solution:
- The system will automatically fix the desync the next time you run
neworlistgen - If you need to force a revision change: delete records with
revision > Nin the DB, then run./cpw listgen
# View recent errors
tail -f log/errors.logIf keys are stored in patcher.conf:
- Run
./cpw rsagento generate new keys - Manually change the data in
keys.json
MIT License - see LICENSE file.
- Fork the repository
- Create a feature branch:
git checkout -b feature/your-feature - Commit your changes + add tests
- Ensure all checks pass:
dart analyze && dart test - Submit a pull request
- Bugs: GitHub Issues
- Email: nikolausgorkun@gmail.com
If you want to support development, click the Sponsor button:
- Use the Boosty link for card payments (RUB/USD/EUR).
- Use the Tronscan link to copy the USDT (TRC-20) wallet address.