Skip to content

Commit 4e57987

Browse files
Added a protocol version compatibility check and fixed setInterruptible() outside event handlers.
The Bot APIs (Java, .NET, Python, TypeScript) now verify the server version from the server handshake against the Bot API version per SemVer (equal majors; equal minors as well for major version 0) and fail with a clear error message on a mismatch, instead of the bot silently standing idle in the battle without scoring. Also fixed a crash (NPE/assertion) in the Java, .NET, and Python Bot APIs when a bot calls setInterruptible() outside an event handler; the call is now ignored as there is no current event to mark as interruptible. The TypeScript Bot API version is stamped into src/version.ts from the /VERSION file by the existing syncVersion Gradle task, same mechanism as package.json. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent a3ad2b2 commit 4e57987

10 files changed

Lines changed: 214 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,20 @@
1+
## [1.0.3] - TBD (not released yet)
2+
3+
### 🐞 Bug Fixes
4+
5+
- Bot API (Java, .NET, Python):
6+
- Fixed a crash when a bot calls `setInterruptible()` outside an event handler. The call is
7+
now simply ignored, as there is no current event to mark as interruptible.
8+
(The TypeScript Bot API was not affected.)
9+
10+
### 🚀 Improvements
11+
12+
- Bot API (Java, .NET, Python, TypeScript):
13+
- A bot now fails with a clear error message when connecting to a server with an incompatible
14+
version, instead of appearing to join the battle but standing idle without scoring.
15+
Per SemVer, the major versions of the Bot API and server must be equal (and the minor
16+
versions as well for major version 0).
17+
118
## [1.0.2] - 2026-05-18 - Minor bug fix for GUI
219

320
### 🐞 Bug Fixes

bot-api/dotnet/api/src/internal/BaseBotInternals.cs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1008,6 +1008,8 @@ private void HandleServerHandshake(string json)
10081008
{
10091009
_serverHandshake = JsonConverter.FromJson<S.ServerHandshake>(json);
10101010

1011+
VerifyServerVersionCompatibility(_serverHandshake?.Version);
1012+
10111013
// Validate bot info before sending bot handshake
10121014
ValidateBotInfo();
10131015

@@ -1020,6 +1022,43 @@ private void HandleServerHandshake(string json)
10201022
_socket.SendTextMessage(text);
10211023
}
10221024

1025+
/// <summary>
1026+
/// Verifies that the server uses a protocol version compatible with this Bot API.
1027+
/// Per SemVer, versions are compatible when the major versions are equal; for the 0.x range
1028+
/// anything may change between minor versions, so there the minor versions must be equal as
1029+
/// well. Without this check, an incompatible server and Bot API silently misinterpret each
1030+
/// other's messages, and the bot appears to join the battle but stands idle without ever
1031+
/// scoring.
1032+
/// </summary>
1033+
private void VerifyServerVersionCompatibility(string serverVersion)
1034+
{
1035+
var apiVersion = typeof(BaseBotInternals).Assembly.GetName().Version;
1036+
var server = ParseMajorMinorVersion(serverVersion);
1037+
if (apiVersion == null || server == null)
1038+
{
1039+
return; // a version is unavailable; cannot verify
1040+
}
1041+
var incompatible = apiVersion.Major != server.Value.Major ||
1042+
(apiVersion.Major == 0 && apiVersion.Minor != server.Value.Minor);
1043+
if (incompatible)
1044+
{
1045+
var message = $"Protocol version mismatch: Bot API version {apiVersion} is not compatible with " +
1046+
$"server version {serverVersion}. The major versions must be equal " +
1047+
"(and the minor versions as well for major version 0).";
1048+
Console.Error.WriteLine(message);
1049+
throw new BotException(message);
1050+
}
1051+
}
1052+
1053+
private static (int Major, int Minor)? ParseMajorMinorVersion(string version)
1054+
{
1055+
if (version == null) return null;
1056+
var match = System.Text.RegularExpressions.Regex.Match(version, @"^\s*(\d+)(?:\.(\d+))?");
1057+
if (!match.Success) return null;
1058+
var minor = match.Groups[2].Success ? int.Parse(match.Groups[2].Value) : 0;
1059+
return (int.Parse(match.Groups[1].Value), minor);
1060+
}
1061+
10231062
private void ValidateBotInfo()
10241063
{
10251064
if (string.IsNullOrWhiteSpace(_botInfo.Name))

bot-api/dotnet/api/src/internal/EventQueue.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,12 @@ internal void ClearEvents()
9090
/// <param name="interruptible">True if the event can be interrupted, false otherwise</param>
9191
internal void SetCurrentEventInterruptible(bool interruptible)
9292
{
93-
EventInterruption.SetInterruptible(_currentTopEvent.GetType(), interruptible);
93+
var currentEvent = _currentTopEvent;
94+
if (currentEvent == null)
95+
{
96+
return; // not inside an event handler; there is no current event to mark as interruptible
97+
}
98+
EventInterruption.SetInterruptible(currentEvent.GetType(), interruptible);
9499
}
95100

96101
private bool IsCurrentEventInterruptible => EventInterruption.IsInterruptible(_currentTopEvent.GetType());

bot-api/java/src/main/java/dev/robocode/tankroyale/botapi/internal/EventQueue.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,11 @@ void clearEvents() {
8181
* @param interruptible true if the event can be interrupted, false otherwise
8282
*/
8383
void setCurrentEventInterruptible(boolean interruptible) {
84-
EventInterruption.setInterruptible(currentTopEvent.getClass(), interruptible);
84+
var currentEvent = currentTopEvent;
85+
if (currentEvent == null) {
86+
return; // not inside an event handler; there is no current event to mark as interruptible
87+
}
88+
EventInterruption.setInterruptible(currentEvent.getClass(), interruptible);
8589
}
8690

8791
private boolean isCurrentEventInterruptible() {

bot-api/java/src/main/java/dev/robocode/tankroyale/botapi/internal/WebSocketHandler.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,8 @@ private void handleServerHandshake(JsonObject jsonMsg) {
241241
var serverHandshake = JsonConverter.fromJson(jsonMsg, ServerHandshake.class);
242242
baseBotInternals.setServerHandshake(serverHandshake);
243243

244+
verifyServerVersionCompatibility(serverHandshake.getVersion());
245+
244246
// Validate bot info before sending bot handshake
245247
validateBotInfo();
246248

@@ -252,6 +254,44 @@ private void handleServerHandshake(JsonObject jsonMsg) {
252254
socket.sendText(msg, true);
253255
}
254256

257+
/**
258+
* Verifies that the server uses a protocol version compatible with this Bot API.
259+
* Per SemVer, versions are compatible when the major versions are equal; for the 0.x range
260+
* anything may change between minor versions, so there the minor versions must be equal as
261+
* well. Without this check, an incompatible server and Bot API silently misinterpret each
262+
* other's messages, and the bot appears to join the battle but stands idle without ever
263+
* scoring.
264+
*/
265+
private void verifyServerVersionCompatibility(String serverVersion) {
266+
var apiVersion = WebSocketHandler.class.getPackage().getImplementationVersion();
267+
var api = parseMajorMinorVersion(apiVersion);
268+
var server = parseMajorMinorVersion(serverVersion);
269+
if (api == null || server == null) {
270+
return; // a version is unavailable (e.g. running from class files); cannot verify
271+
}
272+
boolean incompatible = api[0] != server[0] || (api[0] == 0 && api[1] != server[1]);
273+
if (incompatible) {
274+
var message = String.format(
275+
"Protocol version mismatch: Bot API version %s is not compatible with server version %s. " +
276+
"The major versions must be equal (and the minor versions as well for major version 0).",
277+
apiVersion, serverVersion);
278+
System.err.println(message);
279+
throw new BotException(message);
280+
}
281+
}
282+
283+
private static int[] parseMajorMinorVersion(String version) {
284+
if (version == null) {
285+
return null;
286+
}
287+
var matcher = java.util.regex.Pattern.compile("^\\s*(\\d+)(?:\\.(\\d+))?").matcher(version);
288+
if (!matcher.find()) {
289+
return null;
290+
}
291+
var minor = matcher.group(2);
292+
return new int[]{Integer.parseInt(matcher.group(1)), minor == null ? 0 : Integer.parseInt(minor)};
293+
}
294+
255295
private void validateBotInfo() {
256296
// If the bot is booted, botInfo might be partially filled by the booter
257297
// but the bot code must ensure name, version, and authors are present.

bot-api/python/src/robocode_tank_royale/bot_api/internal/event_queue.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,10 @@ def set_current_event_interruptible(self, interruptible: bool) -> None:
5959
Args:
6060
interruptible: True if the current event can be interrupted; false otherwise.
6161
"""
62-
assert self.current_top_event is not None, "No current event to set interruptible state for."
63-
EventInterruption.set_interruptible(type(self.current_top_event), interruptible)
62+
current_event = self.current_top_event
63+
if current_event is None:
64+
return # not inside an event handler; there is no current event to mark as interruptible
65+
EventInterruption.set_interruptible(type(current_event), interruptible)
6466

6567
def is_current_event_interruptible(self) -> bool:
6668
"""Checks if the current event can be interrupted by new events with higher priority.

bot-api/python/src/robocode_tank_royale/bot_api/internal/websocket_handler.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,8 @@ async def handle_server_handshake(self, json_msg: Dict[Any, Any]) -> None:
268268
server_handshake: ServerHandshake = from_json(json_msg) # type: ignore
269269
self.base_bot_internals.server_handshake = server_handshake
270270

271+
self._verify_server_version_compatibility(server_handshake.version)
272+
271273
# Validate bot info before sending bot handshake
272274
self._validate_bot_info()
273275

@@ -320,6 +322,48 @@ def _transfer_std_out_to_bot_intent(self) -> None:
320322
else:
321323
self.base_bot_internals.bot_intent.std_err = None
322324

325+
def _verify_server_version_compatibility(self, server_version: Optional[str]) -> None:
326+
"""Verifies that the server uses a protocol version compatible with this Bot API.
327+
328+
Per SemVer, versions are compatible when the major versions are equal; for the 0.x
329+
range anything may change between minor versions, so there the minor versions must be
330+
equal as well. Without this check, an incompatible server and Bot API silently
331+
misinterpret each other's messages, and the bot appears to join the battle but stands
332+
idle without ever scoring.
333+
"""
334+
try:
335+
from importlib.metadata import version as _package_version
336+
337+
api_version: Optional[str] = _package_version("robocode-tank-royale")
338+
except Exception:
339+
return # version unavailable; cannot verify
340+
api = self._parse_major_minor(api_version)
341+
server = self._parse_major_minor(server_version)
342+
if api is None or server is None:
343+
return
344+
incompatible = api[0] != server[0] or (api[0] == 0 and api[1] != server[1])
345+
if incompatible:
346+
message = (
347+
f"Protocol version mismatch: Bot API version {api_version} is not compatible "
348+
f"with server version {server_version}. The major versions must be equal "
349+
f"(and the minor versions as well for major version 0)."
350+
)
351+
import sys
352+
353+
print(message, file=sys.stderr)
354+
raise BotException(message)
355+
356+
@staticmethod
357+
def _parse_major_minor(version: Optional[str]) -> Optional[tuple[int, int]]:
358+
if not version:
359+
return None
360+
import re
361+
362+
match = re.match(r"\s*(\d+)(?:\.(\d+))?", version)
363+
if not match:
364+
return None
365+
return int(match.group(1)), int(match.group(2) or 0)
366+
323367
def _validate_bot_info(self) -> None:
324368
"""Validate bot info before sending handshake to server."""
325369
if self._is_blank(self.bot_info.name):

bot-api/typescript/build.gradle.kts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,13 @@ tasks {
2424

2525
val syncVersion by registering {
2626
group = "build"
27-
description = "Synchronises package.json version with gradle.properties"
27+
description = "Synchronises package.json and src/version.ts version with gradle.properties"
2828
val pkgFile = file("package.json")
29+
val versionTsFile = file("src/version.ts")
2930
val newVersion = project.version.toString()
3031
inputs.property("version", newVersion)
3132
outputs.file(pkgFile)
33+
outputs.file(versionTsFile)
3234
doLast {
3335
val content = pkgFile.readText()
3436
val updated = content.replace(
@@ -37,6 +39,14 @@ tasks {
3739
)
3840
pkgFile.writeText(updated)
3941
logger.lifecycle("Updated package.json version to $newVersion")
42+
43+
val versionTsContent = versionTsFile.readText()
44+
val updatedVersionTs = versionTsContent.replace(
45+
Regex("""API_VERSION = "[^"]+""""),
46+
"""API_VERSION = "$newVersion""""
47+
)
48+
versionTsFile.writeText(updatedVersionTs)
49+
logger.lifecycle("Updated src/version.ts version to $newVersion")
4050
}
4151
}
4252

bot-api/typescript/src/WebSocketHandler.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { API_VERSION } from "./version.js";
12
import { BotInfo } from "./BotInfo.js";
23
import { BotHandshakeFactory } from "./BotHandshakeFactory.js";
34
import { EnvVars } from "./EnvVars.js";
@@ -154,6 +155,8 @@ export class WebSocketHandler {
154155
this.serverHandshake = msg;
155156
this.callbacks.onServerHandshake?.(msg);
156157

158+
this.verifyServerVersionCompatibility(msg.version);
159+
157160
// Validate required bot info before sending the bot handshake
158161
this.validateBotInfo();
159162

@@ -204,6 +207,44 @@ export class WebSocketHandler {
204207
// ---------------------------------------------------------------------------
205208

206209
/** Validates that required bot info fields are set before sending the bot handshake. */
210+
/**
211+
* Verifies that the server uses a protocol version compatible with this Bot API.
212+
* Per SemVer, versions are compatible when the major versions are equal; for the 0.x range
213+
* anything may change between minor versions, so there the minor versions must be equal as
214+
* well. Without this check, an incompatible server and Bot API silently misinterpret each
215+
* other's messages, and the bot appears to join the battle but stands idle without ever
216+
* scoring.
217+
*/
218+
private verifyServerVersionCompatibility(serverVersion?: string): void {
219+
const api = this.parseMajorMinorVersion(API_VERSION);
220+
const server = this.parseMajorMinorVersion(serverVersion);
221+
if (api == null || server == null) {
222+
return; // a version is unavailable; cannot verify
223+
}
224+
const incompatible = api[0] !== server[0] || (api[0] === 0 && api[1] !== server[1]);
225+
if (incompatible) {
226+
const message =
227+
`Protocol version mismatch: Bot API version ${API_VERSION} is not compatible with ` +
228+
`server version ${serverVersion}. The major versions must be equal ` +
229+
"(and the minor versions as well for major version 0).";
230+
console.error(message);
231+
throw new BotException(message);
232+
}
233+
}
234+
235+
private parseMajorMinorVersion(version?: string): [number, number] | null {
236+
if (version == null) {
237+
return null;
238+
}
239+
const match = /^\s*(\d+)(?:\.(\d+))?/.exec(version);
240+
const major = match?.[1];
241+
if (major == null) {
242+
return null;
243+
}
244+
const minor = match?.[2];
245+
return [parseInt(major, 10), minor == null ? 0 : parseInt(minor, 10)];
246+
}
247+
207248
private validateBotInfo(): void {
208249
if (this.isBlank(this.botInfo.name)) {
209250
this.throwMissingPropertyException("name");

bot-api/typescript/src/version.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/**
2+
* The version of this Bot API.
3+
*
4+
* Do not edit manually — the version is controlled by the /VERSION file in the repository root
5+
* and stamped into this file by the Gradle `syncVersion` task (same mechanism as package.json).
6+
*/
7+
export const API_VERSION = "1.0.2";

0 commit comments

Comments
 (0)