Introduction
This book holds documentation for Photon/PUN, Bullet Force's internals and BulletForceHaxV3, a cheat for Bullet Force.
Bullet Force is a multiplayer first-person shooter game published by Blayze Games. It has released for web, Android and iOS), with a native PC port coming to Steam in the near future.
Why BulletForceHax?
Bullet Force uses Photon Unity Networking, or PUN, for networking, meaning that most of the networking logic isn't implemented by the Bullet Force developers but part of a pre-made package. PUN is also client-authoritive in many aspects, requiring specific hooks/plugins by the game developer to add additional security measures instead of being secure by default.
The game's name is BulletForce.
Additionally, Bullet Force targets Unity's WebGL platform which means traditional hacking techniques such as code injection and memory manipulation are less trivial. It allows for experimenting with different approaches as there is no single way that's clearly better than the rest.
These factors make Bullet Force a fun target to reverse engineer and write tooling for. It's unlike games that cheats are traditionally written for like Assault Cube or Counter-Strike 2 where the same techniques carry over between games, and to the author's knowledge had not seen any open source cheats until the original release of BulletForceHax.
BulletForceHaxV2 as since succeeded BulletForceHax, and later BulletForceHaxV3 as succeeded it, which is where current focus lies. This book won't cover old iterations of BulletForceHax and you shouldn't expect any further development or support for them.
Tech stack
Bullet Force uses the Unity game engine, compiled to various platforms using IL2CPP which transpiles the managed .NET assemblies the C# compiler produces into C++ code, which is then compiled to the target platform (being WebAssembly for WebGL, x86-64 for PC and ARM for Android and iOS).
For networking, Bullet Force uses Photon Unity Networking (PUN). This section of the book won't explain PUN in detail. As of 2025-04-23, Bullet Force uses v4.0.29.11263
of the Photon OnPremise Plugins SDK.
Bullet Force also uses various other libraries. These may be listed at a later point.
HTTP API
Bullet Force has a HTTP API that's mostly used for account-related operations. It's hosted at https://server.blayzegames.com/OnlineAccountSystem/
and appears to be a PHP server proxied behind Cloudflare.
This book won't cover any potential exploits in the Bullet Force server that could lead to information disclosure, denial of service, arbitrary code execution or other traditional security vulnerabilities. This book is a guide to Bullet Force and Photon internals, it's not a guide on how to illegally compromise server infrastructure you don't own.
Readers are strongly encouraged to responsibly disclose any security issues to the Bullet Force developers, and are reminded that exploiting such security issues is illegal and can result in legal penalties.
OpenAPI definition
BulletForceHax provides documentation of the Bullet Force HTTP API surface in the form of an OpenAPI definition with accompanying Swagger UI interface. BulletForceHax developers maintain this OpenAPI definition on a best-effort basis, meaning it may not be up to date or may miss various significant endpoints.
Generating OpenAPI clients
One use for OpenAPI definitions is to automatically generate API clients. These clients allow programmatic access to the API through a more high-level API with code that's generated at build-time.
The Bullet Force API is problematic in this regard because it returns a text/html
content-types for JSON-formatted responses. OpenAPI client generators can't reasonably predict this, and some use a generic byte stream or string response type instead. You can work around this in several ways:
- Modify the OpenAPI definition to specify
application/json
responses instead. This tricks the client generator into generating JSON parsing code, although this causes problems with generated clients that check the response's return type at runtime. - Manually parse the returned byte stream or string into the correct data type. The OpenAPI definition still contains all return types, so client generators should include those models in their generate code too.
OpenAPI definition
Below is a copy of the full OpenAPI definition as it was while during compilation of this book. You can also find this hosted online or in the GitHub repository.
openapi: 3.0.3 # latest version supported by progenitor
info:
title: Bullet Force
description: |-
This OpenAPI definition describes the internal Bullet Force API.
Note that this is a best-effort replication of the API. It may be inaccurate or incomplete.
contact:
name: Variant9
url: https://github.com/holly-hacker/bulletforcehaxv3
# TODO: license
version: 1.0.0
servers:
- url: https://server.blayzegames.com/OnlineAccountSystem
tags:
- name: Account
description: API calls related to accounts and authentication
- name: Chat
description: API calls related to lobby chat
paths:
# Login
/login.php:
post:
summary: Checks if a login is valid
description: |-
Checks whether a login is valid and returns some info about the account.
operationId: login
tags: ["Account"]
requestBody:
required: true
content:
application/x-www-form-urlencoded:
schema:
type: object
properties:
username:
$ref: "#/components/schemas/UserName"
password:
$ref: "#/components/schemas/UserPassword"
store:
type: string
description: Which game client is used to execute this request
example: "BALYZE_WEB" # typo is expected
useJSON:
type: boolean
description: Determines the response type
example: true
locale:
type: string
description: The user's language
example: "english"
tutorialr:
type: number
example: 1
crazyGamesToken:
type: string
description: Key is present but has no value
responses:
"200":
description: Success and error responses?
content:
text/html:
schema:
type: object
properties:
acnumber:
type: string
format: number
example: "123456789"
isTutorial:
type: number
format: boolean
example: 0
locale:
type: string
example: "english"
popup:
type: boolean
example: false
status:
type: number
example: 1
/get_multiplayer_auth_code.php:
post:
summary: Get a code to authenticate a user in a match
description: |-
Returns an authentication code that should be sent using the [RpcSendMultiplayerAuthToken](https://variant9.dev/BulletForceHaxV3/BulletForceInternal/PUN/ListOfRpcCalls.html#RpcSendMultiplayerAuthToken) RPC call.
This token is only valid until a new token is generated.
operationId: getMultiplayerAuthCode
tags: ["Account"]
requestBody:
required: true
content:
application/x-www-form-urlencoded:
schema:
type: object
properties:
username:
$ref: "#/components/schemas/UserName"
required: true
password:
$ref: "#/components/schemas/UserPassword"
required: true
responses:
"200":
description: This request always returns 200, regardless of success.
content:
text/html:
schema:
type: string
examples:
success:
description: "Success response, containing a hex-encoded token"
value: "12345678abcde"
error:
description: "Error response, containing an empty string"
value: ""
# Chat
/get_lobby_chatV2.php:
post:
summary: Fetch in-game lobby chat
description: |-
Returns the in-game lobby chat which is seen when loading into the game.
Note that this is not the chat you see while in a match. In a match, chat messages are received using the [RpcSendChatMessage](https://variant9.dev/BulletForceHaxV3/BulletForceInternal/PUN/ListOfRpcCalls.html#RpcSendChatMessage) RPC call.
operationId: getLobbyChatV2
tags: ["Chat"]
requestBody:
required: true
content:
application/x-www-form-urlencoded:
schema:
type: object
properties:
platform:
type: string
description: The platform the user is using
example: WebGLPlayer
username:
$ref: "#/components/schemas/UserName"
required: true # works without this but prints error
password:
$ref: "#/components/schemas/UserPassword"
server:
type: string
format: uri
description: The lobby server the user is using
example: wss://game-ca-1.blayzegames.com
responses:
"200":
description: |-
Contains JSON-encoded messages and some metadata.
Note that the content type is `text/html`, but the content is encoded as JSON.
content:
text/html: # BF does not set correct content type headers
schema:
type: object
properties:
messages:
type: array
maxItems: 50
items:
$ref: "#/components/schemas/ChatMessage"
playersOnline:
type: string
format: number
minimum: 0
playersOnlineInServer:
type: number
minimum: 0
/send_lobby_chatV2.php:
post:
summary: Send a chat message
description: |-
Used to send a chat message. Returns a list of the new chat messages in response.
operationId: sendLobbyChatV2
tags: ["Chat"]
requestBody:
required: true
content:
application/x-www-form-urlencoded:
schema:
type: object
properties:
platform:
type: string
description: The platform the user is using
example: WebGLPlayer
username:
$ref: "#/components/schemas/UserName"
password:
$ref: "#/components/schemas/UserPassword"
chat:
type: string
format: uri
description: The chat message to send
example: "Hello world!"
server:
type: string
format: uri
description: The lobby server the user is using
example: wss://game-ca-1.blayzegames.com
responses:
"200":
description: |-
Contains JSON-encoded messages, including the newly sent message.
Note that the content type is `text/html`, but the content is encoded as JSON.
content:
text/html: # BF does not set correct content type headers
schema:
type: object
properties:
messages:
type: array
maxItems: 50
items:
$ref: "#/components/schemas/ChatMessage"
components:
schemas:
UserName:
type: string
description: The player's username
example: "PC-Username"
UserPassword:
type: string
description: The player's password hashed with SHA512, encoded as uppercase hexadecimal.
example: "B109F3BBBC244EB82441917ED06D618B9008DD09B3BEFD1B5E07394C706A8BB980B1D7785E5976EC049B46DF5F1326AF5A2EA6D103FD07C95385FFAB0CACBC86"
ChatMessage:
type: object
properties:
message:
type: string
description: The content of the chat message. This includes the sender's username.
example: "PlayerName: <color=silver>Hello world!</color>"
time:
type: number
format: timestamp
description: The Unix timestamp when the message was sent, in seconds.
example: 1735689600
username:
type: string
description: The username of the person that sent the chat message.
example: "PlayerName"
externalDocs:
url: https://variant9.dev/BulletForceHaxV3/
List of RPC calls
The RPC indices can be found through UABEA and checking the PhotonServerSettings
MonoBehavior resource.
Signatures for RPC calls can be found in a generated DummyDLL by looking for methods with the PunRPC
attribute applied. For example:
// PlayerScript
[PunRPC]
public void AcknowledgeDamageDoneRPC(string status, float damage, int victimID)
{
// ...
}
The parameters to this function are the parameters sent in the RPC event.
In some cases, these methods may take an additional parameter PhotonMessageInfo
parameter. This parameter isn't sent as part of the RPC event:
// PlayerScript
[PunRPC]
private void HealthUpdated(float value, PhotonMessageInfo info)
{
// ...
}
Full list
0: AcknowledgeDamageDoneRPC(string status, float damage, int victimID)
Class: PlayerScript
status
is most likely a stringified version of PlayerHitPlayerStatus
.
1: AnotherRPCMethod(?)
Not found in source code.
2: BecomeNewMasterClient()
Class: MasterClientFinder
3: ChangeCrouchState(?)
4: Chat(?)
Class: InRoomChat
5: CmdGetTeamNumber(?)
6: ColorRpc(?)
Class: OnClickRequestOwnership
7: DestroyRpc(?)
Class: OnClickDestroy
8: DisplayVoteData(?)
9: DoJump(?)
Class: JumpAndRunMovement
10: FetchCheaters(?)
11: FetchVoteData(?)
12: FlagOwnerTeamUpdated(?)
13: FlagTakenValueUpdated(?)
14: Flash()
Class: OnClickFlashRpc
15: GetBestSpawnPointForPlayer(int flagIDToSpawnOn)
Class: PlayerScript
16: GotKillAssist(float amount, int killedID)
Class: PlayerScript
17: HealthUpdated(float value)
Class: PlayerScript
18: InstantiateRpc(int viewID)
Class: ManualPhotonViewAllocator
19: JSNow()
Class: PlayerScript
20: KickPlayer(string playerToKick, string hashedpass)
Class: PlayerScript
21: LatencyReceive(?)
Class: MasterClientFinder
22: LatencySend(?)
Class: MasterClientFinder
23: localCreateGrenade(Vector3 position, Vector3 velocity, float forcedDelay, byte grenadeWeaponType)
Class: PlayerScript
24: localHurt(int damagerID, float damage, Vector3 localPosition, byte damagerWeapon, float newHealth)
Class: PlayerScript
25: localReload()
Class: PlayerScript
26: localSpawnThrowingWeapon(Vector3 position, Vector3 velocity, byte weaponType)
Class: PlayerScript
27: MapVotedFor(?)
28: Marco(?)
Not found in source code.
29: MatchOverChanged(bool value)
Class: PlayerScript
30: mpMeleeAnimation()
Class: PlayerScript
31: mpThrowGrenadeAnimation()
Class: PlayerScript
32: MyRPCMethod(?)
Not found in source code.
33: NukeKill()
Class: PlayerScript
34: PickupItemInit(double timeBase, float[] inactivePickupsAndTimes)
Class: PickupItemSyncer
35: PlayerHitPlayer(?)
36: PlayerKickedForPing(short ping)
Class: PlayerScript
37: Polo(?)
38: PunPickup(?)
Class: PickupItem
39: PunPickupSimple()
Class: PickupItemSimple
40: PunRespawn()
/ PunRespawn(Vector3 pos)
Class: PickupItem
It seems like this method has multiple overloads, and can be called either with or without position parameter.
41: ReliabilityMessageReceived(?)
42: ReliabilityMessageSent(?)
43: RequestForPickupItems()
Class: PickupItemSyncer
44: RequestForPickupTimes()
Class: PickupItemSyncer
45: RequestVipsOnMasterFromSubordinate(?)
46: RestartHardcoreModeRound(?)
47: RestartMatch(?)
48: RpcDie(int killedPlayerID, int killerID, byte killerHealth, byte killerWeapon, bool headshot)
Class: PlayerScript
49: RPCElevatorButtonPressed(?)
50: RpcSendChatMessage(string msgUsername, string msg, short r, short g, sthor b)
Class: PlayerScript
Puts a message in chat in the format similar to {sender name}: <color=#rgb>{msg}</color>
.
The msgUsername
parameter seems to be completely ignored. Instead, the game uses the name from the sender's PlayerProperties
.
If the sender's name is empty, the :
section of the chat messages is also removed which allows the sender to place any message in any color in the chat. However, the sender's name can only be empty before the auth token is sent, which means the sender only has a brief window to do this before they are kicked.
The game will occasionally send out an ad through this RPC call: RpcSendChatMessage("[Bullet Force]", "Consider supporting the game by buying something in the shop!", 255i16, 255i16, 0i16)
. This message is sent by a player, likely the game's host.
51: RpcShoot(int actorID, float damage, Vector3 position, Vector3 direction, byte numberOfBullets, byte spread, double timeShot, int weaponType)
Class: PlayerScript
52: RpcShowHitmarker()
Class: PlayerScript
53: RpcShowPerkMessage(string msgUsername, string msg)
Example: RpcShowPerkMessage("PlayerName", " used Counter UAV")
Class: PlayerScript
54: SetElevatorsClosed(?)
55: SetMaps(?)
56: SetNextMap(?)
57: SetPing(short p)
Class: PlayerScript
58: SetRank(byte r)
Changes the player's rank in the player list.
Class: PlayerScript
59: SetSpawnPoint(Vector3 s, int spawnedPlayerActorNr)
Class: PlayerScript
60: SetTimeScale(float s)
Supposedly sets the time scale.
When executed as a fresh joined player, this either crashes the lobby or kicks the player (to be checked).
Class: PlayerScript
61: ShowAnnouncement(string text, float time)
Shows a red piece of text on-screen for all players. You do not need to be authenticated for this RPC call, but you need to have instantiated a PlayerBody
.
Class: PlayerScript
62: ShowDebugCapsule(Vector3 pos)
Class: PlayerScript
63: SpawnFailed(?)
64: TaggedPlayer(?)
65: TeleportToPosition(Vector3 position)
Class: PlayerScript
66: UpdateAlivePlayers(int _team0Alive, int _team1Alive)
Class: MatchManager
67: UpdateHMFFARounds(int playerID, int roundsWon)
Class: PlayerScript
68: UpdateMPDeaths(int value)
Class: PlayerScript
69: UpdateMPKills(int value)
Class: PlayerScript
70: UpdateMPRounds(int value)
Class: PlayerScript
71: UpdateTeamNumber(byte value)
Class: ManualPhotonViewAllocator
72: UpdateTeamPoints(?)
73: UpdateTimeInMatch(?)
74: UpdateVIPsOnSubordinates(?)
75: UsernameChanged(string value)
Class: PlayerScript
76: WeaponCamoChanged(int value)
Class: PlayerScript
77: WeaponTypeChanged(byte value)
Class: PlayerScript
78: RpcACKill(?)
79: RpcForceKillstreak(KillstreakManager.Killstreak k, bool isOnTheSameTeam)
The first argument is most likely sent as an int
, as that's the default type for enums in C#.
public enum Killstreak
{
None = 0,
UAV = 1,
SuperSoldier = 2,
CounterUAV = 3,
AdvancedUAV = 4,
Haste = 5,
Nuke = 6,
}
Class: PlayerScript
80: RpcDownloadScriptable(?)
81: DecreaseCountDown()
Class: RPCDataSendingMasterClientToMaster
82: IncreaseNumber()
Class: RPCDataSendingMasterClientToMaster
83: SendNewCountDownToClients()
Class: RPCDataSendingMasterClientToMaster
84: SetKD(float kd)
Class: PlayerScript
85: RpcHitVerified(string shotID, bool verified, string info)
Class: PlayerScript
86: RpcVerifyHit(string shotID, int damagerID, int damagedPlayerID, int weaponTypeID, float bulletStartPosX, float bulletStartPosY, float bulletStartPosZ, float bulletHitPosX, float bulletHitPosY, float bulletHitPosZ)
Class: PlayerScript
87: RpcRespawned(Vector3 spawnPoint)
Class: PlayerScript
88: RpcSendMultiplayerAuthToken(string token)
This RPC method has to be called using the response string from API endpoint https://server.blayzegames.com/OnlineAccountSystem/get_multiplayer_auth_code.php
.
If this method isn't called, the player gets kicked from the game. If this gets called with an invalid auth token such as for example an unknown token, a token from another player or when a newer token has generated, then the player also gets kicked from the game.
89: RpcRequestEnterVehicle(?)
Class: BFVehicle
90: RpcEnteredVehicle(?)
Class: BFVehicle
91: RpcGetKicked(string reason)
Tells a player they've been kicked. The client does not validate the sender of this packet.
Class: PlayerScript
92: RpcReportHack(int reportID, int hackerID, int hackType)
Class: PlayerScript
93: RpcPlayerHitPlayerHitmarker(int damagerID, int damagedPlayerID, byte weaponType, bool headshot)
Class: PlayerScript
94: AllPlayersInRoomExist(?)
Not found in source code.
95: CheckPlayers(?)
Not found in source code.
96: ReportPlayerHacking(?)
Not found in source code.
97: UpdatePlayerVector3(?)
98: SetKeysValues(?)
99: RpcDamageVehicle(?)
Class: BFVehicle
100: RpcVehicleDestroyed(?)
Class: BFVehicle
101: BFS_SetSpawnPointForPlayer(?)
102: BFS_GetBestSpawnPointForPlayer(?)
103: BFS_PlayerLeftRoom(?)
104: BFS_FireBullet(?)
105: BFS_DamagePlayer(?)
106: RpcSendReceiveCustomMapToServer(?)
Example: RpcSendReceiveCustomMapToServer("53779")
107: RpcCustomMapLikeDislike(?)
Getting copies of game files
There are various files that may be useful while reverse engineering Photon Unity Networking or Bullet Force. This page describes how to get some of them.
Bullet Force
Bullet Force WebGL (web build)
You can download the WebGL game files by inspecting network traffic through your browser's developer tools or a HTTP/HTTPS proxy.
The copy of the game hosted on CrazyGames serves the following files which may be of interest:
- https://files.crazygames.com/bullet-force-multiplayer/321/Build/bfweb4.loader.js
- https://files.crazygames.com/bullet-force-multiplayer/321/Build/bfweb4.framework.js.gz
- https://files.crazygames.com/bullet-force-multiplayer/321/Build/bfweb4.data.gz
- https://files.crazygames.com/bullet-force-multiplayer/321/Build/bfweb4.wasm.gz
The location of these files may, and are likely to, change in the future. To ensure you have the latest game files, find the latest URLs through DevTools.
Bullet Force APK (Android build)
You can get a copy of the Bullet Force APK from APKMirror. You may find multiple variants even within a version number, but pick the one uploaded last. You can expect this file to be ~500mb in size.
You should end up with an .apkm
file, which is a "split" APK. You can change the file extension to .zip
and extract it which should result in a directory structure similar to this:
.
├── APKM_installer.url
├── base.apk
├── icon.png
├── info.json
├── META-INF
│ ├── APKMIRRO.RSA
│ ├── APKMIRRO.SF
│ └── MANIFEST.MF
├── split_config.arm64_v8a.apk
├── split_config.armeabi_v7a.apk
└── split_UnityDataAssetPack.apk
Photon
Photon Unity Networking
You can download a copy of Photon Unity Networking by installing Unity, creating a new project, and installing it from the Asset Store.
You may also be able to find uploaded copies on GitHub.
Photon Server
Photon uses an on-premise version of the Photon Server. This handles all server-side logic, excluding the Bullet Force-specific extensions/plugins. It can be useful to provide insight into how Photon handles connections on the server side, and could perhaps be used to create a privately hosted server.
The current v5.x.x.x
Photon Server SDK seems to only be accessible to paying Photon Circle Members which starts at $125/month with a minimum initial term of 3 months.
You may be able to find copies uploaded online, such as this v4.0.25.11263
version from 2023 which used to be available as a free download on the Photon website. As Bullet Force appears to be using v4 of PUN, this version is likely fairly close to what Bullet Force uses.