From a43ebcdd642437078e7fdefa8e2eeeed5e95180f Mon Sep 17 00:00:00 2001 From: Mark Kropf Date: Thu, 18 Jun 2026 19:58:36 +0100 Subject: [PATCH 01/90] SolLua: use sol::lua_nil instead of the sol::nil alias A handful of SolLua bindings still referenced sol::nil / sol::type::nil while the rest of the same files already use sol::lua_nil. sol::nil is a thin alias for lua_nil (see lib/sol2/sol.hpp) and collides with the `nil` macro that Objective-C headers define, so lua_nil is the portable spelling. This brings the remaining spots in line with the prevailing convention. Extracted from cc3cad907f in #2991; cross-platform, no behavior change. --- rts/Rml/SolLua/bind/Context.cpp | 6 +++--- rts/Rml/SolLua/bind/Element.cpp | 2 +- rts/Rml/SolLua/bind/bind.cpp | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/rts/Rml/SolLua/bind/Context.cpp b/rts/Rml/SolLua/bind/Context.cpp index db0dcb8960b..bd40fd4e535 100644 --- a/rts/Rml/SolLua/bind/Context.cpp +++ b/rts/Rml/SolLua/bind/Context.cpp @@ -131,7 +131,7 @@ struct lua_iterator_state sol::state_view l{s}; int index = 0; int count = keytable.size(); - while (keytable.get(++index).get_type() != sol::type::nil && index <= count) { + while (keytable.get(++index).get_type() != sol::type::lua_nil && index <= count) { this->keys.emplace_back(sol::object(l, sol::in_place, index)); } } else { @@ -199,7 +199,7 @@ createNewIndexFunction(std::shared_ptr data, const } if (value.is()) { auto value_raw = value.as().raw_get("__raw"); - if (value_raw != sol::nil && value_raw.is()) { + if (value_raw != sol::lua_nil && value_raw.is()) { // new value is a datamodel proxy, so get the underlying table to assign prop.as().raw_set(solkey, value_raw.as().call(value)); } else { @@ -290,7 +290,7 @@ sol::table openDataModel(Rml::Context& self, const Rml::String& name, sol::objec } if (value.is()) { auto value_raw = value.as().raw_get("__raw"); - if (value_raw != sol::nil && value_raw.is()) { + if (value_raw != sol::lua_nil && value_raw.is()) { // new value is a datamodel proxy, so get the underlying table to assign data->Table.raw_set(key, value_raw.as().call(value)); } else { diff --git a/rts/Rml/SolLua/bind/Element.cpp b/rts/Rml/SolLua/bind/Element.cpp index e2edf19159a..3bf3ecc7fea 100644 --- a/rts/Rml/SolLua/bind/Element.cpp +++ b/rts/Rml/SolLua/bind/Element.cpp @@ -158,7 +158,7 @@ namespace Rml::SolLua void Set(const sol::this_state L, const std::string& name, const sol::object& value) { - if (value.get_type() == sol::type::nil) { + if (value.get_type() == sol::type::lua_nil) { m_element->RemoveProperty(name); return; } diff --git a/rts/Rml/SolLua/bind/bind.cpp b/rts/Rml/SolLua/bind/bind.cpp index 21eabe9d46c..98ce9fec37c 100644 --- a/rts/Rml/SolLua/bind/bind.cpp +++ b/rts/Rml/SolLua/bind/bind.cpp @@ -39,7 +39,7 @@ namespace Rml::SolLua sol::object makeObjectFromVariant(const Rml::Variant* variant, sol::state_view s) { - if (!variant) return sol::make_object(s, sol::nil); + if (!variant) return sol::make_object(s, sol::lua_nil); switch (variant->GetType()) { @@ -69,10 +69,10 @@ namespace Rml::SolLua case Rml::Variant::VOIDPTR: return sol::make_object(s, variant->Get()); default: - return sol::make_object(s, sol::nil); + return sol::make_object(s, sol::lua_nil); } - return sol::make_object(s, sol::nil); + return sol::make_object(s, sol::lua_nil); } } // end namespace Rml::SolLua From 3e83e20c2805b09c1165442e4df428fcb935a7a5 Mon Sep 17 00:00:00 2001 From: Tom J Nowell Date: Thu, 18 Jun 2026 20:17:54 +0100 Subject: [PATCH 02/90] float3: drop redundant direct streflop_cond.h include float3.h included lib/streflop/streflop_cond.h directly, ahead of FastMath.h. FastMath.h defines MATH_SQRT_OVERRIDE before it includes streflop_cond.h (so streflop does not define its own math::sqrt(float) -- FastMath provides a faster one). The direct include meant streflop_cond.h could be processed before that override was set. Drop the direct include; FastMath.h pulls in streflop_cond.h transitively with the override in place. creg_cond.h keeps its original position -- it does not pull in streflop, so it does not need to move. Surfaced by the macOS port (#2991, commit cc3cad907f). Co-authored-by: Mark Kropf --- rts/System/float3.h | 1 - 1 file changed, 1 deletion(-) diff --git a/rts/System/float3.h b/rts/System/float3.h index f096c112c0d..bf47dd90c2a 100644 --- a/rts/System/float3.h +++ b/rts/System/float3.h @@ -9,7 +9,6 @@ #include #include "System/BranchPrediction.h" -#include "lib/streflop/streflop_cond.h" #include "System/creg/creg_cond.h" #include "System/FastMath.h" #include "System/type2.h" From 517608f4076b2071742badd6a90eab3dc6262828 Mon Sep 17 00:00:00 2001 From: Tom J Nowell Date: Fri, 19 Jun 2026 00:58:16 +0100 Subject: [PATCH 03/90] SafeUtil: include and directly (#3025) `SafeUtil.h` uses `` for `std::addressof`, and `` for `std::is_trivially_copyable` / `std::is_trivially_constructible_v`, but pulled them in only transitively. Include them directly so the header is self-contained and does not rely on include order elsewhere. Also use `std::is_trivially_default_constructible` for the default-construction. Extracted from cc3cad907f in #2991; cross-platform, no behavior change. Co-authored-by: Mark Kropf Co-authored-by: sprunk --- rts/System/SafeUtil.h | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/rts/System/SafeUtil.h b/rts/System/SafeUtil.h index ce19b3f9738..9fbf946cc82 100644 --- a/rts/System/SafeUtil.h +++ b/rts/System/SafeUtil.h @@ -5,6 +5,8 @@ #include #include +#include +#include namespace spring { template inline void SafeDestruct(T*& p) @@ -112,8 +114,8 @@ namespace spring { static_assert(sizeof(TIn) == sizeof(TOut), "Types must match sizes"); static_assert(std::is_trivially_copyable::value , "Requires TriviallyCopyable input"); static_assert(std::is_trivially_copyable::value, "Requires TriviallyCopyable output"); - static_assert(std::is_trivially_constructible_v, - "This implementation additionally requires destination type to be trivially constructible"); + static_assert(std::is_trivially_default_constructible::value, + "This implementation additionally requires destination type to be trivially default-constructible"); TOut t2; std::memcpy(std::addressof(t2), std::addressof(t1), sizeof(TIn)); From 6318fb6c445daddeb1c9f4dea27e69bfb13847e1 Mon Sep 17 00:00:00 2001 From: iamaperson000 Date: Thu, 18 Jun 2026 19:49:28 +0100 Subject: [PATCH 04/90] LuaTextures: log glTexImage failures instead of returning nil silently LuaTextures::Create returned an empty string on glTexImage failure with no diagnostics. Log target/size/format/dataFormat/dataType/glError so texture-creation failures can be diagnosed. Extracted from 1e75080731 in #2991; cross-platform, no behavior change beyond the added log. --- rts/Lua/LuaTextures.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rts/Lua/LuaTextures.cpp b/rts/Lua/LuaTextures.cpp index 8d3a083dbef..0ae691d63c0 100644 --- a/rts/Lua/LuaTextures.cpp +++ b/rts/Lua/LuaTextures.cpp @@ -90,7 +90,9 @@ std::string LuaTextures::Create(const Texture& tex) } break; } - if (glGetError() != GL_NO_ERROR) { + if (const GLenum texErr = glGetError(); texErr != GL_NO_ERROR) { + LOG_L(L_ERROR, "[LuaTextures::%s] glTexImage failed: target=0x%x size=%dx%d fmt=0x%x dataFmt=0x%x dataType=0x%x border=%d glError=0x%x", + __func__, tex.target, tex.xsize, tex.ysize, tex.format, dataFormat, dataType, tex.border, texErr); glDeleteTextures(1, &texID); glBindTexture(tex.target, currentBinding); return ""; From 54348ff04d3fa6d1583224d3568cd2cf5825a2ab Mon Sep 17 00:00:00 2001 From: bruno-dasilva <8520801+bruno-dasilva@users.noreply.github.com> Date: Thu, 18 Jun 2026 21:11:02 -0700 Subject: [PATCH 05/90] fix: guard against already-dead reclaim targets (#3020) Engine crash thread: https://discordapp.com/channels/549281623154229250/1516994684591931535 Bugged Scenario: A reclaimer A is guarding another reclaimer B, while both reclaiming the same wreck. A can be notified of wreck death BEFORE the guarded unit B is notified, leading to trying to reclaim the same (actively-deleting) wreck because B's reference hasn't been cleaned up yet. This leads to an eventual segfault. So: the short term/easy fix is to skip reclaiming of a dead/dying target. --- rts/Sim/Units/CommandAI/BuilderCAI.cpp | 4 ++-- rts/Sim/Units/UnitTypes/Builder.cpp | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/rts/Sim/Units/CommandAI/BuilderCAI.cpp b/rts/Sim/Units/CommandAI/BuilderCAI.cpp index b747e342615..e1faddb7dd6 100644 --- a/rts/Sim/Units/CommandAI/BuilderCAI.cpp +++ b/rts/Sim/Units/CommandAI/BuilderCAI.cpp @@ -891,13 +891,13 @@ void CBuilderCAI::ExecuteGuard(Command& c) StopSlowGuard(); } return; - } else if (b->curReclaim && owner->unitDef->canReclaim) { + } else if (b->curReclaim && !b->curReclaim->detached && owner->unitDef->canReclaim) { StopSlowGuard(); if (!ReclaimObject(b->curReclaim)) { StopMove(); } return; - } else if (b->curResurrect && owner->unitDef->canResurrect) { + } else if (b->curResurrect && !b->curResurrect->detached && owner->unitDef->canResurrect) { StopSlowGuard(); if (!ResurrectObject(b->curResurrect)) { StopMove(); diff --git a/rts/Sim/Units/UnitTypes/Builder.cpp b/rts/Sim/Units/UnitTypes/Builder.cpp index 4cf3240b1cd..deb8c846b73 100644 --- a/rts/Sim/Units/UnitTypes/Builder.cpp +++ b/rts/Sim/Units/UnitTypes/Builder.cpp @@ -605,6 +605,15 @@ void CBuilder::SetRepairTarget(CUnit* target) void CBuilder::SetReclaimTarget(CSolidObject* target) { RECOIL_DETAILED_TRACY_ZONE; + // A target already being destroyed (detached) cannot have a death dependence + // registered (AddDeathDependence no-ops on detached objects), which would leave + // curReclaim dangling. This happens when ~CObject's DependentDied cascade re-enters + // reclaim logic mid-deletion (e.g. CBuilderCAI::ExecuteGuard reading a guardee's + // stale curReclaim) on the very feature being freed. Refuse the dead target. + assert(target != nullptr); + if (target->detached) + return; + if (dynamic_cast(target) != nullptr && !static_cast(target)->def->reclaimable) return; @@ -630,6 +639,11 @@ void CBuilder::SetReclaimTarget(CSolidObject* target) void CBuilder::SetResurrectTarget(CFeature* target) { RECOIL_DETAILED_TRACY_ZONE; + // see SetReclaimTarget: never depend on an object that is already being destroyed + assert(target != nullptr); + if (target->detached) + return; + if (curResurrect == target || target->udef == nullptr) return; @@ -646,6 +660,11 @@ void CBuilder::SetResurrectTarget(CFeature* target) void CBuilder::SetCaptureTarget(CUnit* target) { RECOIL_DETAILED_TRACY_ZONE; + // see SetReclaimTarget: never depend on an object that is already being destroyed + assert(target != nullptr); + if (target->detached) + return; + if (target == curCapture) return; From e91a58f5c57dcdd25df3e51aac911ce3b481179f Mon Sep 17 00:00:00 2001 From: Tom J Nowell Date: Sat, 20 Jun 2026 14:14:07 +0100 Subject: [PATCH 06/90] CUtils: use const dirent* for the scandir selector on all platforms util_fileSelector was declared with a non-const struct dirent* on __APPLE__ and const elsewhere. macOS scandir() expects the selector argument as int(*)(const struct dirent*), so the non-const Apple variant failed to compile under GCC: Util.c:500: error: passing argument 3 of 'scandir' from incompatible pointer type Both branches were otherwise identical, so drop the __APPLE__ split and use const struct dirent* unconditionally (matches POSIX scandir). No effect on Linux/Windows. Assisted by Claude Code; verified by compiling the macOS headless build. --- AI/Wrappers/CUtils/Util.c | 4 ---- 1 file changed, 4 deletions(-) diff --git a/AI/Wrappers/CUtils/Util.c b/AI/Wrappers/CUtils/Util.c index c33d2e418e8..4764a619900 100644 --- a/AI/Wrappers/CUtils/Util.c +++ b/AI/Wrappers/CUtils/Util.c @@ -487,11 +487,7 @@ static void util_initFileSelector(const char* suffix) { fileSelectorSuffix = suffix; } -#if defined(__APPLE__) -static int util_fileSelector(struct dirent* fileDesc) { -#else static int util_fileSelector(const struct dirent* fileDesc) { -#endif return util_endsWith(fileDesc->d_name, fileSelectorSuffix); } From 580100a0dfeb3fb2f6203ad0cf7bf077f212b6d4 Mon Sep 17 00:00:00 2001 From: Tom J Nowell Date: Sat, 20 Jun 2026 14:12:15 +0100 Subject: [PATCH 07/90] legacy: only require X11 on non-Apple UNIX The legacy build did find_package(X11 REQUIRED) under a plain if(UNIX) guard. macOS is UNIX in CMake but does not use X11 (it uses Cocoa), so configuring the engine on macOS failed at find_package(X11). An if(APPLE) block already follows for Foundation, so exclude Apple from the X11 branch: if(UNIX AND NOT APPLE). Surfaced configuring the spring-headless target on macOS (which reuses the legacy Game target). No effect on Linux/Windows. Assisted by Claude Code; verified by configuring the macOS build. --- rts/builds/legacy/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rts/builds/legacy/CMakeLists.txt b/rts/builds/legacy/CMakeLists.txt index cbcc906f83d..f1d19ede7e3 100644 --- a/rts/builds/legacy/CMakeLists.txt +++ b/rts/builds/legacy/CMakeLists.txt @@ -49,7 +49,7 @@ find_freetype_hack() # hack to find different named freetype.dll find_package_static(Freetype 2.8.1 REQUIRED) list(APPEND engineLibraries Freetype::Freetype) -if (UNIX) +if (UNIX AND NOT APPLE) find_package(X11 REQUIRED) target_link_libraries(Game PRIVATE X11::Xcursor) list(APPEND engineLibraries ${X11_Xcursor_LIB} ${X11_X11_LIB}) From d13dcd341361637cdf1ab4e65b0e8d6fa7b5ca21 Mon Sep 17 00:00:00 2001 From: Tom J Nowell Date: Sat, 20 Jun 2026 13:10:24 +0100 Subject: [PATCH 08/90] FindSevenZip: also accept the 7zz binary name Modern 7-Zip ships its CLI as '7zz' (Homebrew's 'sevenzip' formula installs /opt/homebrew/bin/7zz; recent Linux distros likewise package '7zz'). FindSevenZip only searched for '7z'/'7za', so configure failed with 'Could NOT find SevenZip (missing: SEVENZIP_BIN)' on such systems. Add '7zz' to the searched NAMES. No effect where 7z/7za already exist. --- rts/build/cmake/FindSevenZip.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rts/build/cmake/FindSevenZip.cmake b/rts/build/cmake/FindSevenZip.cmake index 1077ec7b17e..0e3e4002a67 100644 --- a/rts/build/cmake/FindSevenZip.cmake +++ b/rts/build/cmake/FindSevenZip.cmake @@ -23,7 +23,7 @@ ENDIF (SEVENZIP_BIN) set(progfilesx86 "ProgramFiles(x86)") find_program(SEVENZIP_BIN - NAMES 7z 7za + NAMES 7z 7za 7zz HINTS "${MINGWDIR}" "${MINGWLIBS}/bin" "$ENV{${progfilesx86}}/7-zip" "$ENV{ProgramFiles}/7-zip" "$ENV{ProgramW6432}/7-zip" PATH_SUFFIXES bin DOC "7zip executable" From 0da81b433a8c4e228a4ac02ac2e943bd0c5aa078 Mon Sep 17 00:00:00 2001 From: Tom J Nowell Date: Fri, 19 Jun 2026 23:55:16 +0100 Subject: [PATCH 09/90] macOS: remove dead SDL 1.2-era SDLMain.{m,h} rts/System/Platform/Mac/SDLMain.m and SDLMain.h are the classic SDL 1.2 Cocoa main wrapper (the Darrell Walisser / Max Horn template). They are: - not listed in any CMakeLists (never compiled) - not #included by any source file - built on Carbon (), which is 32-bit-only and unavailable on modern macOS / Apple Silicon SDL2 provides its own SDL_main, so this wrapper is obsolete. Remove the dead files so the macOS platform layer reflects what is actually built. --- rts/System/Platform/Mac/SDLMain.h | 18 -- rts/System/Platform/Mac/SDLMain.m | 299 ------------------------------ 2 files changed, 317 deletions(-) delete mode 100644 rts/System/Platform/Mac/SDLMain.h delete mode 100644 rts/System/Platform/Mac/SDLMain.m diff --git a/rts/System/Platform/Mac/SDLMain.h b/rts/System/Platform/Mac/SDLMain.h deleted file mode 100644 index 995d036c3f0..00000000000 --- a/rts/System/Platform/Mac/SDLMain.h +++ /dev/null @@ -1,18 +0,0 @@ -/* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ - -/* - * SDLMain.m - main entry point for our Cocoa-ized SDL app - * Initial Version: Darrell Walisser - * Non-NIB-Code & other changes: Max Horn - * Feel free to customize this file to suit your needs. - */ - -#ifdef __APPLE__ - -#import - -@interface SDLMain : NSObject -@end - -#endif - diff --git a/rts/System/Platform/Mac/SDLMain.m b/rts/System/Platform/Mac/SDLMain.m deleted file mode 100644 index 2fea69074ad..00000000000 --- a/rts/System/Platform/Mac/SDLMain.m +++ /dev/null @@ -1,299 +0,0 @@ -/* SDLMain.m - main entry point for our Cocoa-ized SDL app - Initial Version: Darrell Walisser - Non-NIB-Code & other changes: Max Horn - - Feel free to customize this file to suit your needs -*/ - -#import "SDL.h" -#import "SDLMain.h" -#import /* for MAXPATHLEN */ -#import -#import - -/* Use this flag to determine whether we use SDLMain.nib or not */ -#define SDL_USE_NIB_FILE 0 - - -static int gArgc; -static char **gArgv; -static BOOL gFinderLaunch; - -//extern NSAutoreleasePool *pool; -//void PreInitMac(); - -void MacMessageBox(const char *msg, const char *caption, unsigned int flags){ - NSAlert *alert = [[[NSAlert alloc] init] autorelease]; - [alert addButtonWithTitle:@"OK"]; - [alert setMessageText:[NSString stringWithCString:caption]]; - [alert setInformativeText:[NSString stringWithCString:msg]]; - [alert setAlertStyle:NSWarningAlertStyle]; - [alert runModal]; -} - -#if SDL_USE_NIB_FILE -/* A helper category for NSString */ -@interface NSString (ReplaceSubString) -- (NSString *)stringByReplacingRange:(NSRange)aRange with:(NSString *)aString; -@end -#else -/* An internal Apple class used to setup Apple menus */ -@interface NSAppleMenuController:NSObject {} -- (void)controlMenu:(NSMenu *)aMenu; -@end -#endif - -@interface SDLApplication : NSApplication -@end - -@implementation SDLApplication -/* Invoked from the Quit menu item */ -- (void)terminate:(id)sender -{ - /* Post a SDL_QUIT event */ - SDL_Event event; - event.type = SDL_QUIT; - SDL_PushEvent(&event); -} -@end - - -/* The main class of the application, the application's delegate */ -@implementation SDLMain - -/* Set the working directory to the .app's parent directory */ -- (void) setupWorkingDirectory:(BOOL)shouldChdir -{ - char parentdir[MAXPATHLEN]; - char *c; - - strncpy ( parentdir, gArgv[0], sizeof(parentdir) ); - c = (char*) parentdir; - - while (*c != '\0') /* go to end */ - c++; - - while (*c != '/') /* back up to parent */ - c--; - - *c++ = '\0'; /* cut off last part (binary name) */ - - if (shouldChdir) - { - assert ( chdir (parentdir) == 0 ); /* chdir to the binary app's parent */ - assert ( chdir ("../../../") == 0 ); /* chdir to the .app's parent */ - } -} - -#if SDL_USE_NIB_FILE - -/* Fix menu to contain the real app name instead of "SDL App" */ -- (void)fixMenu:(NSMenu *)aMenu withAppName:(NSString *)appName -{ - NSRange aRange; - NSEnumerator *enumerator; - NSMenuItem *menuItem; - - aRange = [[aMenu title] rangeOfString:@"SDL App"]; - if (aRange.length != 0) - [aMenu setTitle: [[aMenu title] stringByReplacingRange:aRange with:appName]]; - - enumerator = [[aMenu itemArray] objectEnumerator]; - while ((menuItem = [enumerator nextObject])) - { - aRange = [[menuItem title] rangeOfString:@"SDL App"]; - if (aRange.length != 0) - [menuItem setTitle: [[menuItem title] stringByReplacingRange:aRange with:appName]]; - if ([menuItem hasSubmenu]) - [self fixMenu:[menuItem submenu] withAppName:appName]; - } - [ aMenu sizeToFit ]; -} - -#else - -void setupAppleMenu(void) -{ - /* warning: this code is very odd */ - NSAppleMenuController *appleMenuController; - NSMenu *appleMenu; - NSMenuItem *appleMenuItem; - - appleMenuController = [[NSAppleMenuController alloc] init]; - appleMenu = [[NSMenu alloc] initWithTitle:@""]; - appleMenuItem = [[NSMenuItem alloc] initWithTitle:@"" action:nil keyEquivalent:@""]; - - [appleMenuItem setSubmenu:appleMenu]; - - /* yes, we do need to add it and then remove it -- - if you don't add it, it doesn't get displayed - if you don't remove it, you have an extra, titleless item in the menubar - when you remove it, it appears to stick around - very, very odd */ - [[NSApp mainMenu] addItem:appleMenuItem]; - [appleMenuController controlMenu:appleMenu]; - [[NSApp mainMenu] removeItem:appleMenuItem]; - [appleMenu release]; - [appleMenuItem release]; -} - -/* Create a window menu */ -void setupWindowMenu(void) -{ - NSMenu *windowMenu; - NSMenuItem *windowMenuItem; - NSMenuItem *menuItem; - - - windowMenu = [[NSMenu alloc] initWithTitle:@"Window"]; - - /* "Minimize" item */ - menuItem = [[NSMenuItem alloc] initWithTitle:@"Minimize" action:@selector(performMiniaturize:) keyEquivalent:@"m"]; - [windowMenu addItem:menuItem]; - [menuItem release]; - - /* Put menu into the menubar */ - windowMenuItem = [[NSMenuItem alloc] initWithTitle:@"Window" action:nil keyEquivalent:@""]; - [windowMenuItem setSubmenu:windowMenu]; - [[NSApp mainMenu] addItem:windowMenuItem]; - - /* Tell the application object that this is now the window menu */ - [NSApp setWindowsMenu:windowMenu]; - - /* Finally give up our references to the objects */ - [windowMenu release]; - [windowMenuItem release]; -} - -/* Replacement for NSApplicationMain */ -void CustomApplicationMain () -{ - NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; - SDLMain *sdlMain; - //PreInitMac(); - - /* Ensure the application object is initialised */ - [SDLApplication sharedApplication]; - - /* Set up the menubar */ - [NSApp setMainMenu:[[NSMenu alloc] init]]; - setupAppleMenu(); - setupWindowMenu(); - - /* Create SDLMain and make it the app delegate */ - sdlMain = [[SDLMain alloc] init]; - [NSApp setDelegate:sdlMain]; - - /* Bring the app to foreground */ - ProcessSerialNumber psn; - GetCurrentProcess(&psn); - TransformProcessType(&psn,kProcessTransformToForegroundApplication); - - /* Start the main event loop */ - [NSApp run]; - - [sdlMain release]; - [pool release]; -} - -#endif - -/* Called when the internal event loop has just started running */ -- (void) applicationDidFinishLaunching: (NSNotification *) note -{ - int status; - - /* Set the working directory to the .app's parent directory */ - [self setupWorkingDirectory:gFinderLaunch]; - -#if SDL_USE_NIB_FILE - /* Set the main menu to contain the real app name instead of "SDL App" */ - [self fixMenu:[NSApp mainMenu] withAppName:[[NSProcessInfo processInfo] processName]]; -#endif - - /* Hand off to main application code */ - status = SDL_main (gArgc, gArgv); - - /* We're done, thank you for playing */ - exit(status); -} -@end - - -@implementation NSString (ReplaceSubString) - -- (NSString *)stringByReplacingRange:(NSRange)aRange with:(NSString *)aString -{ - unsigned int bufferSize; - unsigned int selfLen = [self length]; - unsigned int aStringLen = [aString length]; - unichar *buffer; - NSRange localRange; - NSString *result; - - bufferSize = selfLen + aStringLen - aRange.length; - buffer = NSAllocateMemoryPages(bufferSize*sizeof(unichar)); - - /* Get first part into buffer */ - localRange.location = 0; - localRange.length = aRange.location; - [self getCharacters:buffer range:localRange]; - - /* Get middle part into buffer */ - localRange.location = 0; - localRange.length = aStringLen; - [aString getCharacters:(buffer+aRange.location) range:localRange]; - - /* Get last part into buffer */ - localRange.location = aRange.location + aRange.length; - localRange.length = selfLen - localRange.location; - [self getCharacters:(buffer+aRange.location+aStringLen) range:localRange]; - - /* Build output string */ - result = [NSString stringWithCharacters:buffer length:bufferSize]; - - NSDeallocateMemoryPages(buffer, bufferSize); - - return result; -} - -@end - - - -#ifdef main -# undef main -#endif - - -/* Main entry point to executable - should *not* be SDL_main! */ -int main (int argc, char **argv) -{ - - /* Copy the arguments into a global variable */ - int i; - - /* This is passed if we are launched by double-clicking */ - if ( argc >= 2 && strncmp (argv[1], "-psn", 4) == 0 ) { - gArgc = 1; - gFinderLaunch = YES; - } else { - gArgc = argc; - gFinderLaunch = NO; - } - gArgv = (char**) malloc (sizeof(*gArgv) * (gArgc+1)); - assert (gArgv != NULL); - for (i = 0; i < gArgc; i++) - gArgv[i] = argv[i]; - gArgv[i] = NULL; - -#if SDL_USE_NIB_FILE - [SDLApplication poseAsClass:[NSApplication class]]; - NSApplicationMain (); -#else - CustomApplicationMain (); -#endif - return 0; -} - - From 83642b066f755ac182ef97ea540569c07e59d2ff Mon Sep 17 00:00:00 2001 From: Scary le Poo Date: Sun, 21 Jun 2026 13:12:50 -0700 Subject: [PATCH 10/90] Fix SplinterFaction card link format Updated the link for the SplinterFaction card to include the 'https://' prefix. This has been broken for a very long time. I've asked Skyrbunny to fix it multiple times, but it has never gotten fixed. --- doc/site/content/_index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/site/content/_index.md b/doc/site/content/_index.md index fcbbe83428f..0d0a8d3b14b 100644 --- a/doc/site/content/_index.md +++ b/doc/site/content/_index.md @@ -19,7 +19,7 @@ draft = false {{< /cards >}} {{< cards >}} {{< card title="Tech Annihilation" image="/showcase/ta.jpeg" link="https://github.com/techannihilation/TA" >}} -{{< card title="SplinterFaction" image="showcase/splinter_faction.jpg" link="splinterfaction.info" >}} +{{< card title="SplinterFaction" image="showcase/splinter_faction.jpg" link="https://splinterfaction.info" >}} {{< card title="Mechcommander: Legacy" image="/showcase/mcl.jpg" link="https://github.com/SpringMCLegacy/SpringMCLegacy/wiki" >}} {{< /cards >}} From 13b6804534543c061d0eb8c06af438cb7d2a1c64 Mon Sep 17 00:00:00 2001 From: bruno-dasilva <8520801+bruno-dasilva@users.noreply.github.com> Date: Mon, 22 Jun 2026 12:38:03 -0700 Subject: [PATCH 11/90] fix: update section min-level in place instead of appending duplicates (#3052) setMinLevel() only searched for an existing entry on the "set back to default" (erase) path. Setting a section to a *non-default* level appended a new row unconditionally, so repeatedly changing one section's level (e.g. via Spring.SetLogSectionFilterLevel) accumulated duplicate entries and eventually filled the fixed 64-slot sectionMinLevels table -- after which every section-level change silently failed with "too many section-levels". Fix: Look the section up before appending and, if it already has an entry, update it in place. This bounds the table at one entry per section. This likely never happens in practice but this was found while writing other tests Co-authored-by: Bruno Da Silva Co-authored-by: Claude Opus 4.8 (1M context) --- rts/System/Log/DefaultFilter.cpp | 21 +++++++++++++------ test/engine/System/Log/TestILog.cpp | 31 +++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/rts/System/Log/DefaultFilter.cpp b/rts/System/Log/DefaultFilter.cpp index cf0c3939d29..3ec33410cf1 100644 --- a/rts/System/Log/DefaultFilter.cpp +++ b/rts/System/Log/DefaultFilter.cpp @@ -157,13 +157,15 @@ void log_filter_section_setMinLevel(int level, const char* section) // (same string but will not become garbage) section = *registeredSection; - if (level == log_filter_section_getDefaultMinLevel(section)) { - using P = decltype(log_filter::sectionMinLevels)::value_type; - - const auto sectionComparer = [](const P& a, const P& b) { return (log_filter_section_compare()(a.first, b.first)); }; - const auto sectionMinLevel = std::lower_bound(secLvls.begin(), secLvls.begin() + log_filter::numLevels, P{section, 0}, sectionComparer); + // locate any existing override for this section (the array is kept sorted) + using P = decltype(log_filter::sectionMinLevels)::value_type; + const auto sectionComparer = [](const P& a, const P& b) { return (log_filter_section_compare()(a.first, b.first)); }; + const auto sectionMinLevel = std::lower_bound(secLvls.begin(), secLvls.begin() + log_filter::numLevels, P{section, 0}, sectionComparer); + const bool exists = (sectionMinLevel != (secLvls.begin() + log_filter::numLevels) && strcmp(sectionMinLevel->first, section) == 0); - if (sectionMinLevel == (secLvls.begin() + log_filter::numLevels) || strcmp(sectionMinLevel->first, section) != 0) + if (level == log_filter_section_getDefaultMinLevel(section)) { + // back to default: drop any existing override (nothing to do otherwise) + if (!exists) return; // erase @@ -175,6 +177,13 @@ void log_filter_section_setMinLevel(int level, const char* section) return; } + // non-default: update any existing override in-place + if (exists) { + sectionMinLevel->second = level; + return; + } + + // add a net new override secLvls[log_filter::numLevels++] = {section, level}; // swap into position diff --git a/test/engine/System/Log/TestILog.cpp b/test/engine/System/Log/TestILog.cpp index cb8d4b3e4a1..7b7d1345f0c 100644 --- a/test/engine/System/Log/TestILog.cpp +++ b/test/engine/System/Log/TestILog.cpp @@ -3,6 +3,7 @@ #include "System/Log/FileSink.h" #include "System/Log/StreamSink.h" #include "System/Log/LogUtil.h" +#include "System/Log/DefaultFilter.h" #include @@ -231,3 +232,33 @@ TEST_CASE("IsEnabled") TLOG_SL( "other-one-time-section", L_DEBUG, "Testing LOG_IS_ENABLED_S"); } + +// Regression for the duplicate-entry leak in log_filter_section_setMinLevel. +// Setting a section to a non-default level used to *append* a new row every call +// instead of updating the existing one, so repeated changes to one section filled +// the fixed-size sectionMinLevels table and then made *all* section-level changes +// silently fail ("too many section-levels"). +TEST_CASE("SectionMinLevelNoDuplicateLeak") +{ + // non-default levels for these (non-default) sections; restored at the end + const int savedDefined = log_filter_section_getMinLevel(LOG_SECTION_DEFINED); + const int savedOneTime = log_filter_section_getMinLevel(LOG_SECTION_ONE_TIME_0); + + // hammer one section far more than the table could ever hold + for (int i = 0; i < 300; ++i) + log_filter_section_setMinLevel((i & 1) ? LOG_LEVEL_WARNING : LOG_LEVEL_ERROR, LOG_SECTION_DEFINED); + + // the most recent value wins (a single, updated-in-place entry) + log_filter_section_setMinLevel(LOG_LEVEL_ERROR, LOG_SECTION_DEFINED); + CHECK(log_filter_section_getMinLevel(LOG_SECTION_DEFINED) == LOG_LEVEL_ERROR); + + // and a *different* section must still be settable: with the old append bug + // the table is saturated by now and this set would be dropped + log_filter_section_setMinLevel(LOG_LEVEL_WARNING, LOG_SECTION_ONE_TIME_0); + CHECK(log_filter_section_getMinLevel(LOG_SECTION_ONE_TIME_0) == LOG_LEVEL_WARNING); + + // restore original levels (setting back to default takes the erase path) + log_filter_section_setMinLevel(savedDefined, LOG_SECTION_DEFINED); + log_filter_section_setMinLevel(savedOneTime, LOG_SECTION_ONE_TIME_0); +} + From 1caa3c52612c9741238f7a086e6d8696c7e77289 Mon Sep 17 00:00:00 2001 From: devgit283 Date: Tue, 23 Jun 2026 09:29:18 +0200 Subject: [PATCH 12/90] Convert `u8string_view` to `string` using explicit size The original code relied on implicit null-termination which is not guaranteed. --- rts/System/FileSystem/FileSystem.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rts/System/FileSystem/FileSystem.cpp b/rts/System/FileSystem/FileSystem.cpp index d1471810f48..f319d2199ca 100644 --- a/rts/System/FileSystem/FileSystem.cpp +++ b/rts/System/FileSystem/FileSystem.cpp @@ -61,7 +61,7 @@ namespace Impl { return std::string(reinterpret_cast(utf8.c_str())); } RECOIL_FORCE_INLINE std::string StoreUTF8AsString(const std::u8string_view& utf8) { - return std::string(reinterpret_cast(utf8.data())); + return std::string(reinterpret_cast(utf8.data()), utf8.size()); } RECOIL_FORCE_INLINE std::string StorePathAsString(const fs::path& path) { return StoreUTF8AsString(path.u8string()); From 2186ee490c027eb2993ce2f67b98d4978ded142e Mon Sep 17 00:00:00 2001 From: devgit283 Date: Fri, 5 Jun 2026 21:57:29 +0200 Subject: [PATCH 13/90] Fix inconsistent class/struct forward declarations Aligns forward declarations with definitions to silence MSVC warnings. --- rts/Rendering/IconHandler.h | 2 +- rts/Sim/Misc/SmoothHeightMesh.h | 2 +- rts/Sim/MoveTypes/Components/MoveTypesComponents.h | 4 ++-- rts/Sim/MoveTypes/MoveDefHandler.h | 2 +- rts/Sim/MoveTypes/Utils/UnitTrapCheckUtils.h | 4 ++-- rts/Sim/Path/HAPFS/PathSearch.h | 2 +- rts/Sim/Path/QTPFS/Components/PathSpeedModInfo.h | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/rts/Rendering/IconHandler.h b/rts/Rendering/IconHandler.h index 6c67749d739..ec0e5bce6e3 100644 --- a/rts/Rendering/IconHandler.h +++ b/rts/Rendering/IconHandler.h @@ -13,7 +13,7 @@ #include "Rendering/GL/RenderBuffersFwd.h" #include "Rendering/Textures/TextureAtlas.h" -class UnitDef; +struct UnitDef; class CTextureRenderAtlas; namespace icon { diff --git a/rts/Sim/Misc/SmoothHeightMesh.h b/rts/Sim/Misc/SmoothHeightMesh.h index 888686af138..91866ddfb6c 100644 --- a/rts/Sim/Misc/SmoothHeightMesh.h +++ b/rts/Sim/Misc/SmoothHeightMesh.h @@ -22,7 +22,7 @@ namespace SmoothHeightMeshNamespace { */ class SmoothHeightMesh { - friend class SmoothHeightMeshDrawer; + friend struct SmoothHeightMeshDrawer; public: diff --git a/rts/Sim/MoveTypes/Components/MoveTypesComponents.h b/rts/Sim/MoveTypes/Components/MoveTypesComponents.h index b0d1d8b4265..fa7380707bb 100644 --- a/rts/Sim/MoveTypes/Components/MoveTypesComponents.h +++ b/rts/Sim/MoveTypes/Components/MoveTypesComponents.h @@ -7,8 +7,8 @@ #include "System/Ecs/Components/BaseComponents.h" #include -struct CUnit; -struct CFeature; +class CUnit; +class CFeature; namespace MoveTypes { diff --git a/rts/Sim/MoveTypes/MoveDefHandler.h b/rts/Sim/MoveTypes/MoveDefHandler.h index fbec124d13c..9c8d1db5601 100644 --- a/rts/Sim/MoveTypes/MoveDefHandler.h +++ b/rts/Sim/MoveTypes/MoveDefHandler.h @@ -18,7 +18,7 @@ class CUnit; class LuaTable; namespace MoveTypes { - class CheckCollisionQuery; + struct CheckCollisionQuery; } namespace MoveDefs { diff --git a/rts/Sim/MoveTypes/Utils/UnitTrapCheckUtils.h b/rts/Sim/MoveTypes/Utils/UnitTrapCheckUtils.h index 7db24ab3835..da7354b01fb 100644 --- a/rts/Sim/MoveTypes/Utils/UnitTrapCheckUtils.h +++ b/rts/Sim/MoveTypes/Utils/UnitTrapCheckUtils.h @@ -3,8 +3,8 @@ #ifndef UNIT_TRAP_CHECK_UTILS_H__ #define UNIT_TRAP_CHECK_UTILS_H__ -struct CFeature; -struct CUnit; +class CFeature; +class CUnit; namespace MoveTypes { void RegisterFeatureForUnitTrapCheck(CFeature* object); diff --git a/rts/Sim/Path/HAPFS/PathSearch.h b/rts/Sim/Path/HAPFS/PathSearch.h index 1fae37cc5dd..fdb3bfc7d85 100644 --- a/rts/Sim/Path/HAPFS/PathSearch.h +++ b/rts/Sim/Path/HAPFS/PathSearch.h @@ -6,7 +6,7 @@ #include "System/float3.h" class CSolidObject; -class MoveDef; +struct MoveDef; namespace HAPFS { struct PathSearch { diff --git a/rts/Sim/Path/QTPFS/Components/PathSpeedModInfo.h b/rts/Sim/Path/QTPFS/Components/PathSpeedModInfo.h index 19f7c9285cc..4bde1a878b5 100644 --- a/rts/Sim/Path/QTPFS/Components/PathSpeedModInfo.h +++ b/rts/Sim/Path/QTPFS/Components/PathSpeedModInfo.h @@ -12,7 +12,7 @@ namespace QTPFS { -class INode; +struct INode; struct NodeLayerSpeedInfoSweep { static constexpr std::size_t page_size = MoveDefHandler::MAX_MOVE_DEFS; From a854046cef9082d73bac86d19c020e4babac4592 Mon Sep 17 00:00:00 2001 From: Tom J Nowell Date: Thu, 25 Jun 2026 15:56:49 +0100 Subject: [PATCH 14/90] macOS: guard X11/Xlib.h include for non-X11 platforms SpringApp.cpp included for every non-Windows platform, but macOS has no X11 headers by default, breaking the native build. The only user of it, XInitThreads(), is already excluded on __APPLE__ at its call site, so guard the include the same way. --- rts/System/SpringApp.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rts/System/SpringApp.cpp b/rts/System/SpringApp.cpp index e5a011b19c3..a2274b6f9e5 100644 --- a/rts/System/SpringApp.cpp +++ b/rts/System/SpringApp.cpp @@ -13,7 +13,9 @@ #undef KeyRelease #else #include // isatty +#ifndef __APPLE__ #include // XInitThreads +#endif #undef KeyPress #undef KeyRelease From ab68d6fc27bcf3a25e06684506b17fb4f9d3bae0 Mon Sep 17 00:00:00 2001 From: bruno-dasilva <8520801+bruno-dasilva@users.noreply.github.com> Date: Mon, 29 Jun 2026 03:26:11 -0700 Subject: [PATCH 15/90] fix: address sse2neon/streflop macro redefinition warning (#2999) --- rts/System/simd_compat.h | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/rts/System/simd_compat.h b/rts/System/simd_compat.h index a405e02f587..c68b59e3484 100644 --- a/rts/System/simd_compat.h +++ b/rts/System/simd_compat.h @@ -3,6 +3,19 @@ #ifdef SSE2NEON #include "lib/sse2neon/sse2neon.h" + // sse2neon leaks 's FE_XXX macros, which collide with the ones streflop + // redefines and trigger a #warning. Undef them here so streflop gets a clean slate. + #undef FE_INVALID + #undef FE_DENORMAL + #undef FE_DIVBYZERO + #undef FE_OVERFLOW + #undef FE_UNDERFLOW + #undef FE_INEXACT + #undef FE_ALL_EXCEPT + #undef FE_TONEAREST + #undef FE_DOWNWARD + #undef FE_UPWARD + #undef FE_TOWARDZERO #else #ifdef _MSC_VER #include // MSVC umbrella From 52b55cf38ac65a4b30257ccc8d0815693da39cac Mon Sep 17 00:00:00 2001 From: Tom J Nowell Date: Mon, 29 Jun 2026 11:34:36 +0100 Subject: [PATCH 16/90] glad: do not build the GLX loader on macOS glad_glx.c includes X11/X.h to provide the GLX windowing-system bindings, but macOS has no X11/GLX. The Glad CMakeLists added glad_glx.c for every UNIX-and-not-MinGW platform, so building any GL-enabled target (e.g. engine-legacy) on macOS failed: fatal error: X11/X.h: No such file or directory macOS resolves GL entry points without GLX (the engine's glxHandler is already #ifdef'd out on __APPLE__), so exclude glad_glx.c on Apple and build only the core glad.c there. --- rts/lib/glad/CMakeLists.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rts/lib/glad/CMakeLists.txt b/rts/lib/glad/CMakeLists.txt index a67e710dd36..7f7391dd7d3 100644 --- a/rts/lib/glad/CMakeLists.txt +++ b/rts/lib/glad/CMakeLists.txt @@ -1,10 +1,10 @@ cmake_minimum_required(VERSION 3.5) project(Glad) -if (UNIX AND NOT MINGW) +if (UNIX AND NOT MINGW AND NOT APPLE) add_library(glad glad.c glad_glx.c) -else (UNIX AND NOT MINGW) +else (UNIX AND NOT MINGW AND NOT APPLE) add_library(glad glad.c) -endif (UNIX AND NOT MINGW) +endif (UNIX AND NOT MINGW AND NOT APPLE) target_include_directories(glad PUBLIC /) \ No newline at end of file From 4034d5a13273ae1078c3aa8b59c525bd6f51c052 Mon Sep 17 00:00:00 2001 From: bruno-dasilva <8520801+bruno-dasilva@users.noreply.github.com> Date: Mon, 29 Jun 2026 05:41:52 -0700 Subject: [PATCH 17/90] docs: add ENGINE_PERFORMANCE.md with some notes on engine internals (#2919) Co-Authored-By: Claude Opus 4.6 (1M context) --- AGENTS.md | 4 +++ coding-agents/ENGINE_PERFORMANCE.md | 55 +++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 coding-agents/ENGINE_PERFORMANCE.md diff --git a/AGENTS.md b/AGENTS.md index 0f87ef9915c..63780f55c46 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -388,6 +388,10 @@ The engine uses custom thread pools. See `THREADPOOL` define and related code. 4. Follow the workflow in `contributing.md` 5. Disclose any AI assistance used +### Additional docs +Please see @coding-agents/ for additional documentation: +- coding-agents/ENGINE_PERFORMANCE.md — notes on scale targets and engine performance internals. Useful for performance related changes. + ## Additional Resources - Official website: https://recoilengine.org diff --git a/coding-agents/ENGINE_PERFORMANCE.md b/coding-agents/ENGINE_PERFORMANCE.md new file mode 100644 index 00000000000..f3bb67f5d77 --- /dev/null +++ b/coding-agents/ENGINE_PERFORMANCE.md @@ -0,0 +1,55 @@ +# Engine performance + +Recoil is an RTS engine built for large-scale games — designed to handle thousands of units at once. + +## Scale target + +- **Target:** ~10k concurrent units, *including buildings*. +- Mobile units tend to be ~40% of that late-game total. +- Largest seen in a real game: ~17.7k units. That's a data point, not a design target. + +## Sim, draw, and update frames + +The main loop is `Update → Draw`, repeating — see the diagram and table below for the per-phase breakdown. Each iteration first drains any queued sim-frame packets (0..N per iteration), then renders one draw frame. `CGame::Update` dispatches `SimFrame()` calls as `NETMSG_NEWFRAME` packets arrive; `CGame::Draw` runs the unsynced update phase and then renders. The sim burst is capped at ~500 ms (`minDrawFPS`) so draw always gets to run, and it's all one thread — sim and rendering are **not concurrent**; parallelism only happens *inside* a phase. + +Conversely, if no sim frames are in the queue the main loop runs `Draw`/`UpdateUnsynced` as fast as possible — many draw iterations can pass between successive sim frames, with visuals interpolating smoothly in between via `globalRendering->timeOffset`. + +``` +main-loop iteration (repeats as fast as possible) +├── CGame::Update (mostly synced) +│ └── SimFrame × 0..N ← processes queued sim frames capped at +| ~500ms per iteration +└── CGame::Draw (unsynced) + ├── UpdateUnsynced ← unsynced update phase + └── render world + screen ← Draw::World + Draw::Screen +``` + +| Phase | Rate | Synced? | Responsibility | +|---|---|---|---| +| **Sim frame** — `CGame::SimFrame` | fixed 30 Hz (`GAME_SPEED`) | mostly yes | advance deterministic state: units, pathing, projectiles, line-of-sight, scripts, Lua `GameFrame` | +| **Draw frame** — `CGame::Draw` | variable | no | update phase (see below) + render world/screen | +| **Update phase** — `CGame::UpdateUnsynced` *(inside draw frame)* | per draw frame | no | timings, interpolation, camera, GUI, sound, world-drawer prep | + +### Profiler buckets + +The engine `CTimeProfiler` (and the `benchmark` tool) report three peer buckets: `Sim` (the whole synced step), `Update` (`CGame::UpdateUnsynced`), and `Draw` (rendering only, *excluding* the Update that runs first). + +`Sim` is **"mostly" synced**: it also bills unsynced work that runs inline during `SimFrame`. +- **Explicit Lua callins** — `GameFrame`/`GameFramePost` run near the start of each sim frame. +- **Event-driven Lua callins** — unsynced widgets can subscribe to synced game events, so their handlers run inline as those events fire during the frame. +- **C++-only unsynced sections** — e.g. the MT projectile visual pass (`Sim::Projectiles::UpdateUnsyncedMT`) and ghosted-building updates (`CUnitDrawer::UpdateGhostedBuildings`). + +### Scheduling and CPU budget + +- Sim has a target rate set by the server; draw is as fast as the hardware allows. The sim target is `30 Hz × speedFactor`; at a speed factor of 1x, in-game time tracks real-world time 1:1, and at 2x speed the server fires twice as many sim frames per real-world second so the world evolves twice as fast. +- **Zero, one, or many** sim frames per draw frame — if the client falls behind, pending sim frames burst in the next iteration to catch up. +- Visuals interpolate between sim frames, so draw rate can exceed sim rate without stutter. +- Sim time is carefully budgeted and scheduled against draw frames (because they run serially) so there's always a minimum fps for the player + +## Multi-threading + +The engine runs one **main thread** plus a pool of **worker threads**, all pinned to distinct cores. We typically aim for 6-8 worker threads. The main thread drives the sim/draw loop; workers pick up parallel work dispatched from the main thread (via `for_mt` and friends in `rts/System/Threading/ThreadPool.h`). The main thread also participates in draining the task queue while it waits. + +Most parallel work in the engine is **homogeneous** — the same operation applied over many items (unit updates, projectile steps, etc.) via `for_mt`. Keeping parallel work homogeneous is a deliberate discipline: it makes determinism easier to reason about and keeps sim output independent of how work happens to land across threads. + +**QTPFS is the one heterogeneous exception.** The quad-tree pathfinder maintains its own per-worker search state (`SearchThreadData`, `SparseData`) independent of engine sim state, which lets it safely run path searches on the worker pool *in the background* via `for_mt_background`. Background tasks yield to higher-priority work by rescheduling themselves when other jobs arrive, so QTPFS soaks up idle worker capacity without preempting foreground parallelism. From 7e07543f7fd14c11ac8df2ef0e4785e081a07aef Mon Sep 17 00:00:00 2001 From: Tom J Nowell Date: Mon, 29 Jun 2026 13:58:30 +0100 Subject: [PATCH 18/90] System: add missing includes for libc++ These three headers use std:: algorithms but rely on being pulled in transitively. libstdc++ does so; libc++ (clang/macOS) does not, so they can fail to compile under libc++ depending on the version. Symbols that require the include: - rts/System/Matrix44f.h -> std::copy - rts/System/SpringHashMap.hpp -> std::fill_n - rts/System/SpringHashSet.hpp -> std::fill_n Adding the include is "include what you use" correctness and is a no-op on toolchains that already provide it transitively. No functional change. Cherry-picked from ExaDev/RecoilEngine (0ed29d550e, 0b4bd10a1e, 3adabd005a). AI assistance: changes identified and applied with Claude (Anthropic); verified by a human (compiled under clang/libc++, no regression). --- rts/System/Matrix44f.h | 1 + rts/System/SpringHashMap.hpp | 1 + rts/System/SpringHashSet.hpp | 1 + 3 files changed, 3 insertions(+) diff --git a/rts/System/Matrix44f.h b/rts/System/Matrix44f.h index 3dd6767dd33..bd5e6d1fc5b 100644 --- a/rts/System/Matrix44f.h +++ b/rts/System/Matrix44f.h @@ -2,6 +2,7 @@ #pragma once +#include #include #include #include diff --git a/rts/System/SpringHashMap.hpp b/rts/System/SpringHashMap.hpp index 1bf487a6111..f6a359cb14d 100644 --- a/rts/System/SpringHashMap.hpp +++ b/rts/System/SpringHashMap.hpp @@ -6,6 +6,7 @@ #pragma once +#include #include #include #include diff --git a/rts/System/SpringHashSet.hpp b/rts/System/SpringHashSet.hpp index 3375e12520c..366a5d35211 100644 --- a/rts/System/SpringHashSet.hpp +++ b/rts/System/SpringHashSet.hpp @@ -6,6 +6,7 @@ #pragma once +#include #include #include // malloc #include From cf3b083040550e9cb6047839624fea829f3ca0c0 Mon Sep 17 00:00:00 2001 From: Tom J Nowell Date: Mon, 29 Jun 2026 10:40:36 +0100 Subject: [PATCH 19/90] DemoTool: link nowide and fmt for their include directories DemoTool compiles engine FileSystem sources (FileHandler, FileSystem) that include nowide/fstream.hpp and fmt/printf.h, but its target_link_libraries omitted the nowide and fmt targets. The engine builds obtain those include directories transitively through the nowide::nowide and fmt::fmt INTERFACE targets; DemoTool linked neither, so the build failed when the headers were not on a default search path (observed building the demotool target on macOS). Link nowide::nowide and fmt::fmt so their INTERFACE include directories propagate to the demotool target. Co-authored-by: Robert Burnham --- tools/DemoTool/CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/DemoTool/CMakeLists.txt b/tools/DemoTool/CMakeLists.txt index dc9cbf66ecf..fdc47e0461d 100644 --- a/tools/DemoTool/CMakeLists.txt +++ b/tools/DemoTool/CMakeLists.txt @@ -70,6 +70,8 @@ target_link_libraries(demotool gflags_nothreads_static 7zip ${ZLIB_LIBRARY} + nowide::nowide + fmt::fmt ${PLATFORM_LIBS} Tracy::TracyClient ) From f898efc6c31b3871cc8f60ed2c567a5de41db696 Mon Sep 17 00:00:00 2001 From: Robert Burnham Date: Thu, 18 Jun 2026 23:30:55 -0500 Subject: [PATCH 20/90] Fix truncated sync checksum in demotool dump --- tools/DemoTool/DemoTool.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/DemoTool/DemoTool.cpp b/tools/DemoTool/DemoTool.cpp index 96bb8930344..d26b0c9b955 100644 --- a/tools/DemoTool/DemoTool.cpp +++ b/tools/DemoTool/DemoTool.cpp @@ -1,5 +1,6 @@ /* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ +#include #include #include #include @@ -417,7 +418,7 @@ void TrafficDump(CDemoReader& reader, bool trafficStats) //uchar myPlayerNum; int frameNum; uint checksum; std::cout << "NETMSG_SYNCRESPONSE: Playernum: "<< (unsigned)buffer[1]; std::cout << " Framenum: " << *(int*)(buffer+2); - std::cout << " Checksum: " << (unsigned)buffer[6]; + std::cout << " Checksum: " << *(uint32_t*)(buffer+6); std::cout << std::endl; break; case NETMSG_DIRECT_CONTROL: From 8141bf23dc42630bb235c155ca7db8ae56ba7610 Mon Sep 17 00:00:00 2001 From: Tom J Nowell Date: Tue, 30 Jun 2026 10:53:26 +0100 Subject: [PATCH 21/90] Portability in archive handlers * avoid parenthesised aggregate init and name the type explicitly (not supported everywhere) * avoid narrowing conversions. --- rts/System/FileSystem/Archives/DirArchive.cpp | 2 +- rts/System/FileSystem/Archives/SevenZipArchive.cpp | 8 ++++---- rts/System/FileSystem/Archives/ZipArchive.cpp | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/rts/System/FileSystem/Archives/DirArchive.cpp b/rts/System/FileSystem/Archives/DirArchive.cpp index 829a56c4251..55242d39db0 100644 --- a/rts/System/FileSystem/Archives/DirArchive.cpp +++ b/rts/System/FileSystem/Archives/DirArchive.cpp @@ -42,7 +42,7 @@ CDirArchive::CDirArchive(const std::string& archiveName) // all variables here will use forward slashes, no need for conversion std::string rawFileName = dataDirsAccess.LocateFile(dirName + origName); - files.emplace_back(origName, std::move(rawFileName), -1, 0); + files.emplace_back(Files{origName, std::move(rawFileName), -1, 0}); // convert to lowercase and store lcNameIndex[StringToLower(std::move(origName))] = static_cast(files.size() - 1); diff --git a/rts/System/FileSystem/Archives/SevenZipArchive.cpp b/rts/System/FileSystem/Archives/SevenZipArchive.cpp index e40ce9e798d..d5994770067 100644 --- a/rts/System/FileSystem/Archives/SevenZipArchive.cpp +++ b/rts/System/FileSystem/Archives/SevenZipArchive.cpp @@ -147,12 +147,12 @@ CSevenZipArchive::CSevenZipArchive(const std::string& name) continue; } - const auto& fd = fileEntries.emplace_back( - i, //fp - SzArEx_GetFileSize(&db, i), // size + const auto& fd = fileEntries.emplace_back(FileEntry{ + static_cast(i), //fp + static_cast(SzArEx_GetFileSize(&db, i)), // size db.MTime.Vals ? static_cast(CTimeUtil::NTFSTimeToTime64(db.MTime.Vals[i].Low, db.MTime.Vals[i].High)) : 0, // modtime std::move(fileName.value()) // origName - ); + }); lcNameIndex.emplace(StringToLower(fd.origName), fileEntries.size() - 1); } diff --git a/rts/System/FileSystem/Archives/ZipArchive.cpp b/rts/System/FileSystem/Archives/ZipArchive.cpp index 37914269c49..e7b13e3e790 100644 --- a/rts/System/FileSystem/Archives/ZipArchive.cpp +++ b/rts/System/FileSystem/Archives/ZipArchive.cpp @@ -58,13 +58,13 @@ CZipArchive::CZipArchive(const std::string& archiveName) unz_file_pos fp{}; unzGetFilePos(zip, &fp); - const auto& fd = fileEntries.emplace_back( + const auto& fd = fileEntries.emplace_back(FileEntry{ std::move(fp), //fp - info.uncompressed_size, //size + static_cast(info.uncompressed_size), //size fName, //origName - info.crc, //crc + static_cast(info.crc), //crc static_cast(CTimeUtil::DosTimeToTime64(info.dosDate)) //modTime - ); + }); lcNameIndex.emplace(StringToLower(fd.origName), fileEntries.size() - 1); } From 898dd6cf869d7c590f7f1bd906fdd40e92f414e3 Mon Sep 17 00:00:00 2001 From: Bruno Da Silva Date: Sat, 11 Apr 2026 17:23:07 +0000 Subject: [PATCH 22/90] docs: add BACKWARDS_COMPATIBILITY.md guidance doc for coding agents --- AGENTS.md | 2 +- coding-agents/BACKWARDS_COMPATIBILITY.md | 25 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 coding-agents/BACKWARDS_COMPATIBILITY.md diff --git a/AGENTS.md b/AGENTS.md index 63780f55c46..f62bffeb42e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -391,7 +391,7 @@ The engine uses custom thread pools. See `THREADPOOL` define and related code. ### Additional docs Please see @coding-agents/ for additional documentation: - coding-agents/ENGINE_PERFORMANCE.md — notes on scale targets and engine performance internals. Useful for performance related changes. - +- coding-agents/BACKWARDS_COMPATIBILITY.md - notes on when we should strive to be backwards compatible. Reference it for any major reworks or api changes. ## Additional Resources - Official website: https://recoilengine.org diff --git a/coding-agents/BACKWARDS_COMPATIBILITY.md b/coding-agents/BACKWARDS_COMPATIBILITY.md new file mode 100644 index 00000000000..d36c3775a78 --- /dev/null +++ b/coding-agents/BACKWARDS_COMPATIBILITY.md @@ -0,0 +1,25 @@ +# Backwards compatibility + +Recoil isn't 100% beholden to backwards compatibility, but breaking changes are weighed carefully against their benefit. Backwards compatiblity is a constraint, not a veto. + +## Why the bar is high + +Recoil games aren't short-cycle Unreal/Unity titles — they're lifetime hobby projects. There is no steady stream of new games picking up the latest engine; the games we have are the games we have. They fall into two camps, and neither absorbs churn well: + +- **Mature games** need stability above all else. +- **Games still in active development** have the flexibility, but rarely the volunteer bandwidth to chase significant engine breakage. + +## How to weigh a change + +- Quantify the benefit (perf, correctness, maintainability) concretely, not in the abstract. +- Identify which games or content would break, and how mechanical the fix is on their side. "Rename a call site" is very different from "rearchitect your gadget." +- Prefer changes whose blast radius is contained or whose adaptation is mechanical. Avoid changes that force games to rethink core logic with no real mitigation path. + +## Precedents + +- **Multi-threaded unit movement & collision** — landed with a large perf win and effectively no game-side impact (ignoring incidentally-fixed bugs). This is the shape of change to look for. +- **Multi-threading `Unit::Update`, `Unit::SlowUpdate`, or projectiles** — don't. The impact on games would be huge and there isn't much that can be done to mitigate it. Not a path worth proposing. + +## The upshot + +Backwards compatiblity constraints don't close the door on performance work — they just point it at the areas where the blast radius is small. Plenty of wins are still on the table; pick the ones games don't have to pay for. From 487fd436e756bdaa5ad4469adbef3f9638bf3101 Mon Sep 17 00:00:00 2001 From: bruno-dasilva <8520801+bruno-dasilva@users.noreply.github.com> Date: Wed, 1 Jul 2026 12:21:24 -0700 Subject: [PATCH 23/90] docs: improve running/testing instructions in AGENTS.md (#2917) --- AGENTS.md | 160 ++++++++++++++++++++++++++------------- docker-build-v2/build.sh | 11 ++- test/AGENTS.md | 128 +++++++++++++++++++++++++++++++ 3 files changed, 245 insertions(+), 54 deletions(-) create mode 100644 test/AGENTS.md diff --git a/AGENTS.md b/AGENTS.md index f62bffeb42e..4d346e24500 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,18 +8,49 @@ RecoilEngine is an open-source real-time strategy (RTS) game engine written in C ## Build Commands +### Submodules + +The repo uses git submodules for vendored libraries (`rts/lib/*`, `tools/pr-downloader`, AI skirmish bots, etc.). If you cloned without `--recurse-submodules`, initialize them before building: +```bash +git submodule update --init --recursive +``` + ### Building the Engine **Using Docker (Recommended):** ```bash -# Build for Linux +# Full build (default: RELWITHDEBINFO, -O3 -g -DNDEBUG, Ninja) +# Output lands in build--/ (e.g. build-amd64-linux/) and the +# ready-to-use install in build-amd64-linux/install/ docker-build-v2/build.sh linux -# Build for Windows +# Parallelism +docker-build-v2/build.sh -j 8 linux + +# Windows cross-build docker-build-v2/build.sh windows -# Build with custom CMake options +# Change optimization level — trailing -D… is forwarded to configure.sh and +# overrides the baked-in RELWITHDEBINFO default. docker-build-v2/build.sh linux -DCMAKE_BUILD_TYPE=DEBUG +docker-build-v2/build.sh linux -DCMAKE_BUILD_TYPE=RELEASE +docker-build-v2/build.sh linux -DCMAKE_BUILD_TYPE=PROFILE + +# Combine cmake options (configure phase) +docker-build-v2/build.sh linux -DBUILD_spring-headless=OFF -DTRACY_ENABLE=ON + +# List all available cmake options and their current values +docker-build-v2/build.sh --configure linux -LH + +# Build a specific target — use --compile so args flow to `cmake --build`, +# not to configure. Without --compile, `-t …` would be rejected by configure. +docker-build-v2/build.sh --compile linux -t engine-headless +docker-build-v2/build.sh --compile linux -t engine-legacy +docker-build-v2/build.sh --compile linux -t tests --verbose + +# Split the phases +docker-build-v2/build.sh --configure linux # configure only +docker-build-v2/build.sh --compile linux # compile only (reuses existing config) ``` **Without Docker:** @@ -27,16 +58,44 @@ docker-build-v2/build.sh linux -DCMAKE_BUILD_TYPE=DEBUG # Create build directory mkdir -p build && cd build -# Configure +# Configure — project requires C++23 (clang ≥ 17 or gcc ≥ 13 on PATH). +# CMAKE_BUILD_TYPE defaults to RELWITHDEBINFO when omitted. cmake .. -# Build specific target -cmake --build . --target engine-headless -j$(nproc) - -# Build all -cmake --build . -j$(nproc) +# Optional: Default generator is Unix Makefiles; add `-G Ninja` for faster builds if ninja is installed. +cmake -G Ninja .. + +# Optional: pin to gcc-13 + gold linker via the in-repo toolchain file +# (tracked under docker-build-v2/; same compiler the docker build uses). +cmake \ + -DCMAKE_TOOLCHAIN_FILE=../docker-build-v2/images/all-linux/toolchain.cmake .. + +# Optional: speed up incremental builds with ccache +cmake \ + -DCMAKE_C_COMPILER_LAUNCHER=ccache \ + -DCMAKE_CXX_COMPILER_LAUNCHER=ccache .. + +# Change optimization level by re-running cmake (no wipe required): +cmake -DCMAKE_BUILD_TYPE=DEBUG .. # no optimization, full symbols +cmake -DCMAKE_BUILD_TYPE=RELEASE .. # optimized, no debug info +cmake -DCMAKE_BUILD_TYPE=RELWITHDEBINFO .. # optimized + debug info (default) +cmake -DCMAKE_BUILD_TYPE=PROFILE .. # optimized + profiling hooks + +# Build (generator-agnostic — works under Ninja or Make) +cmake --build . + +# Build a specific target +cmake --build . --target engine-headless +cmake --build . --target engine-legacy +cmake --build . --target engine-dedicated +cmake --build . --target tests ``` +> The docker flow writes to **`build--/`** (e.g. `build-amd64-linux/` +> or `build-amd64-windows/`), which is a different directory than the `build/` +> used by this flow. When running tests, point commands at whichever build +> directory you populated. + ### Build Types - `DEBUG` - Debug build with full symbols and no optimization - `RELEASE` - Optimized release build @@ -44,55 +103,64 @@ cmake --build . -j$(nproc) - `PROFILE` - Profiling build ### Build Targets -- `engine-legacy` - Main engine build -- `engine-headless` - Headless server build -- `engine-dedicated` - Dedicated server build -- `tests` - Build all test executables -- `check` - Build and run all tests -- `spring-content` - Build game content packages +- `engine-legacy` — main interactive engine build +- `engine-headless` — headless engine (no graphics) +- `engine-dedicated` — dedicated server +- `unitsync` — unitsync shared library +- `pr-downloader` — content downloader tool +- `tests` — phony; builds every `test_*` executable under `build/test/` +- `check` — phony; depends on `engine-headless` + all `test_*` executables, then runs ctest with `--output-on-failure -V` +- `install` — install into `CMAKE_INSTALL_PREFIX` ## Testing -### Test Framework -The project uses **Catch2** for unit testing. Test files are located in the `test/` directory. +### Writing Tests +See `test/AGENTS.md` for details on writing tests, available compile flags, patterns, and test helpers. ### Running Tests **Build and run all tests:** ```bash -# From build directory -make tests # Build all test executables -make check # Build and run all tests via CTest -make test # Alternative: run via CTest +# From build/ — ctest / check recipes below assume a non-docker build. +# For a docker build, run `docker-build-v2/build.sh --compile linux -t check` +# (runs ctest inside the container) or invoke the binaries in +# build-amd64-linux/test/ directly. +cmake --build . --target tests # build all test executables (no run) +ctest # run all tests (does not rebuild) +# OR +cmake --build . --target check # rebuild engine-headless + all tests, then run ctest -V ``` -**Run a single test:** +`check` is the safe default when iterating; bare `ctest` is faster when nothing relevant has changed since the last build. + +**Run a single test (from repo root):** ```bash -# Tests are built as executable binaries in the build directory -# Pattern: test_ +# Tests are built as executable binaries under /test/ +# Pattern: /test/test_ +# where depends on if you built in docker or not (see above). -# Run specific test executable -./test_Float3 -./test_Matrix44f -./test_SyncedPrimitive -./test_UDPListener +./build/test/test_Float3 +./build/test/test_Matrix44f +./build/test/test_SyncedPrimitive +./build/test/test_UDPListener -# Run with verbose output -./test_Float3 -s +# Catch2: show passing assertions too +./build/test/test_Float3 -s -# Run specific test case -./test_Float3 "TestSection" +# Run a specific test case by name (positional arg matches TEST_CASE name, supports wildcards) +./build/test/test_Float3 "Float3" +./build/test/test_Float3 "Float34_*" ``` -**Run via CTest:** +**Run via CTest (from inside build/):** ```bash -# Run specific test by name -ctest -R Float3 -V +# Filter by regex, show output only on failure +ctest -R Float3 --output-on-failure -# Run with regex pattern -ctest -R Matrix -V +# Same, but verbose (full stdout regardless of result) +ctest -R Float3 -V -# List all available tests +# List all registered tests without running ctest -N ``` @@ -325,20 +393,6 @@ Use preprocessor directives for platform-specific code: - Use tabs for indentation in CMake files - Keep lines reasonably short -### Adding Tests -In `test/CMakeLists.txt`: -```cmake -set(test_name TestName) -set(test_src - "${CMAKE_CURRENT_SOURCE_DIR}/path/to/TestFile.cpp" - ${test_Common_sources} -) -set(test_libs - library_name -) -add_spring_test(${test_name} "${test_src}" "${test_libs}" "${test_flags}") -``` - ## Project Structure - `rts/` - Main engine source code diff --git a/docker-build-v2/build.sh b/docker-build-v2/build.sh index 765ba4a1495..6bded8b8b57 100755 --- a/docker-build-v2/build.sh +++ b/docker-build-v2/build.sh @@ -179,7 +179,16 @@ if [[ "$GIT_DIR" != "$GIT_COMMON_DIR" ]]; then WORKTREE_MOUNTS="-v $GIT_COMMON_DIR:$GIT_COMMON_DIR:ro" fi -$RUNTIME run --platform=linux/$ARCH -it --rm \ +# Docker's -t requires stdin AND stdout to be TTYs; in CI, pipes, or agent +# contexts one or both are missing and docker errors out with "the input +# device is not a TTY". Only add -t when it's safe; -i is harmless either +# way (non-interactive stdin just sees EOF). +TTY_FLAG= +if [[ -t 0 && -t 1 ]]; then + TTY_FLAG=-t +fi + +$RUNTIME run --platform=linux/$ARCH -i $TTY_FLAG --rm \ -v "$CWD${P}":/build/src:z,ro \ -v "$CWD${P}.cache${P}ccache-$PLATFORM":/build/cache:z,rw \ -v "$CWD${P}build-$PLATFORM":/build/out:z,rw \ diff --git a/test/AGENTS.md b/test/AGENTS.md new file mode 100644 index 00000000000..17fe17159f0 --- /dev/null +++ b/test/AGENTS.md @@ -0,0 +1,128 @@ +# AGENTS.md + +This file provides guidance to coding agents when working with code in this repository's test folder. + +## Build & Run Tests + +```bash +# From build/: build all test executables +cmake --build . --target tests + +# From build/: run tests. ctest/check recipes here assume a non-docker +# build. For a docker build, run `docker-build-v2/build.sh --compile linux -t check` +# (runs ctest inside the container) or invoke binaries in +# build-amd64-linux/test/ directly (see below). +ctest # run already-built tests; does not rebuild +cmake --build . --target check # rebuild engine-headless + all tests first, then ctest -V + +# From build/: run ctest with filters +ctest --output-on-failure # concise; only shows failing output +ctest -R Float3 --output-on-failure # filter by name regex +ctest -R Float3 -V # same, verbose +ctest -N # list all registered tests without running + +# From repo root: run a single test binary directly (fastest iteration). +# Use build-amd64-linux/ instead of build/ if you built via docker. +./build/test/test_Float3 +./build/test/test_Float3 -s # Catch2: show passing assertions too +./build/test/test_Float3 "Float3" # filter by TEST_CASE name (supports wildcards) +``` + +`cmake --build . --target check` is the full-fat target: it depends on `engine-headless` and +every `test_*` executable, so it relinks anything stale before running ctest with +`--output-on-failure -V`. Use bare `ctest` when you want to skip the rebuild. + +## Framework + +**Catch2** (amalgamated single-header version) in `lib/catch2/`. Custom main in `lib/catch2/catch_main.cpp` with leak detection enabled via `CATCH_AMALGAMATED_CUSTOM_MAIN`. + +## Test Organization + +``` +engine/System/ # Core system tests (math, threading, I/O, serialization) +engine/Sim/Misc/ # Simulation tests (QuadField, Ellipsoid) +lib/luasocket/ # Lua socket restriction tests +other/ # Mutex benchmarks, memory pool tests +unitsync/ # UnitSync API tests +validation/ # Integration tests (shell scripts that run full game simulation) +tools/CompileFailTest/ # Negative test framework (tests that must NOT compile) +headercheck/ # Header isolation tests (cmake -DHEADERCHECK=ON) +``` + +## Adding a New Test + +1. Create test source in the appropriate subdirectory under `engine/`, `other/`, etc. +2. In `test/CMakeLists.txt`, add a block using the `add_spring_test` macro: +```cmake +set(test_name MyTest) +set(test_src + "${CMAKE_CURRENT_SOURCE_DIR}/engine/System/testMyTest.cpp" + ${test_Common_sources} +) +set(test_libs "") +set(test_flags "-DNOT_USING_CREG -DNOT_USING_STREFLOP -DBUILDING_AI") +add_spring_test(${test_name} "${test_src}" "${test_libs}" "${test_flags}") +``` +3. The macro creates executable `test_` and registers it with ctest as `test`. + +## Common Compile Flags + +| Flag | Purpose | +|------|---------| +| `-DUNIT_TEST` | Always set for all tests (global) | +| `-DSYNCCHECK` | Always set for all tests (global) | +| `-DNOT_USING_CREG` | Stubs out `CR_*` macros. Default unless the test exercises save/load serialization. | +| `-DNOT_USING_STREFLOP` | Falls back to ``. Default unless the test verifies synced floating-point determinism. | +| `-DBUILDING_AI` | Makes engine headers skip engine-only paths. Pair with `NOT_USING_CREG` and `NOT_USING_STREFLOP`. | +| `-DTHREADPOOL` | Selects the real thread pool over the stub. Set only when the test needs real parallelism. | +| `-DUNITSYNC` | Marks the file as part of unitsync. Only needed for tests that link the unitsync library. | + +## Patterns + +### Basic test file +```cpp +#include +#include "System/Log/ILog.h" + +TEST_CASE("MyFeature") { + CHECK(1 + 1 == 2); + SECTION("sub-case") { + CHECK(true); + } +} +``` + +### Tests that need timing +```cpp +#include "System/Misc/SpringTime.h" +TEST_CASE("TimingTest") { + InitSpringTime ist; // RAII - must be instantiated before using spring_time + // ... +} +``` + +### Thread-safe assertions +Catch2 is NOT thread-safe. Multi-threaded tests must guard assertions: +```cpp +static spring::mutex m; +#define SAFE_CHECK(expr) { std::lock_guard lk(m); CHECK(expr); } +``` + +### Compile-fail tests +Tests that verify code correctly fails to compile. Source uses `#ifdef FAIL` guards: +```cpp +#ifdef FAIL +#ifdef TEST1 + int x = someStronglyTypedEnum; // must not compile +#endif +#endif +``` +Registered in CMakeLists.txt via: +```cmake +spring_test_compile_fail(testName_fail1 ${test_src} "-DTEST1") +``` + +## Test Helpers (mock/stub files) + +- `engine/System/NullGlobalConfig.cpp` — provides default `globalConfig` without full engine init +- `engine/System/Nullerrorhandler.cpp` — stubs `ErrorMessageBox()` to prevent GUI popups From d313449ce9fc71b65280285651162f4061fdf171 Mon Sep 17 00:00:00 2001 From: sprunk Date: Thu, 2 Jul 2026 10:47:46 +0200 Subject: [PATCH 24/90] Move `isHeadless` from `Engine` to `Platform`. (#3067) --- doc/site/content/changelogs/_index.markdown | 2 +- rts/Lua/LuaConstEngine.cpp | 3 --- rts/Lua/LuaConstPlatform.cpp | 4 ++++ 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/doc/site/content/changelogs/_index.markdown b/doc/site/content/changelogs/_index.markdown index 55096582c3a..0e073538cb7 100644 --- a/doc/site/content/changelogs/_index.markdown +++ b/doc/site/content/changelogs/_index.markdown @@ -74,7 +74,7 @@ This is the bleeding-edge changelog since version 2025.06, for **pre-release 202 - add `Spring.GetClosestEnemyUnit(x, y, z, range = inf, allyTeamID, bool useLoS = true, bool spherical = false, bool requireEnemyToSeePos = false) → unitID?` to LuaRules. - large QTPFS perf improvements. - always output logs to stdout. -- add `Engine.isHeadless`, available in unsynced only. +- add boolean `Platform.isHeadless`. - archive cache version 20 → 21. ## Fixes diff --git a/rts/Lua/LuaConstEngine.cpp b/rts/Lua/LuaConstEngine.cpp index 068a26d0ecf..0d38c1ffbf0 100644 --- a/rts/Lua/LuaConstEngine.cpp +++ b/rts/Lua/LuaConstEngine.cpp @@ -59,9 +59,6 @@ bool LuaConstEngine::PushEntries(lua_State* L) LuaPushNamedString(L, "buildFlags" , SpringVersion::GetAdditional()); LuaPushNamedNumber(L, "wordSize", (!CLuaHandle::GetHandleSynced(L))? Platform::NativeWordSize() * 8: 0); - if (!CLuaHandle::GetHandleSynced(L)) - LuaPushNamedBool(L, "isHeadless", SpringVersion::IsHeadless()); - LuaPushNamedNumber(L, "gameSpeed", GAME_SPEED); LuaPushNamedNumber(L, "maxCustomPaletteID", MAX_CUSTOM_COLORS - 1); diff --git a/rts/Lua/LuaConstPlatform.cpp b/rts/Lua/LuaConstPlatform.cpp index 0a5f667ed80..7262bd7d5a5 100644 --- a/rts/Lua/LuaConstPlatform.cpp +++ b/rts/Lua/LuaConstPlatform.cpp @@ -2,6 +2,7 @@ #include "LuaConstPlatform.h" #include "LuaUtils.h" +#include "Game/GameVersion.h" #include "System/Platform/Hardware.h" #include "System/Platform/Misc.h" #include "Rendering/GlobalRendering.h" @@ -147,5 +148,8 @@ bool LuaConstPlatform::PushEntries(lua_State* L) /*** @field Platform.macAddrHash string */ LuaPushNamedString(L, "macAddrHash", Platform::GetMacAddrHash()); + /*** @field Platform.isHeadless boolean Is this a headless build which only simulates and doesnt offer interactive IO? */ + LuaPushNamedBool(L, "isHeadless", SpringVersion::IsHeadless()); + return true; } From 092cb0a04ef4db8230bf781fbdc2a01d74a0c6a3 Mon Sep 17 00:00:00 2001 From: Alexander Heistermann Date: Thu, 2 Jul 2026 18:04:17 -0500 Subject: [PATCH 25/90] Add Lua trace ray functions (#1624) * adds `Spring.TraceRayBetweenPositions(xA, yA, zA, xB, yB, zB, type)` * adds `Spring.TraceRayInDirection(x, y, z, dx, dy, dz, length, type)` * type is a string, "unit", "feature", or "both" * both return an array of `{distance, objID, objType}` sorted by increasing distance --- rts/Lua/LuaSyncedRead.cpp | 138 ++++++++++++++++++++++++++++++++++++++ rts/Lua/LuaSyncedRead.h | 5 +- 2 files changed, 140 insertions(+), 3 deletions(-) diff --git a/rts/Lua/LuaSyncedRead.cpp b/rts/Lua/LuaSyncedRead.cpp index 40b25741b52..3983a71f3ff 100644 --- a/rts/Lua/LuaSyncedRead.cpp +++ b/rts/Lua/LuaSyncedRead.cpp @@ -399,6 +399,8 @@ bool LuaSyncedRead::PushEntries(lua_State* L) REGISTER_LUA_CFUNC(TraceRayGroundInDirection); REGISTER_LUA_CFUNC(TraceRayGroundBetweenPositions); + REGISTER_LUA_CFUNC(TraceRayInDirection); + REGISTER_LUA_CFUNC(TraceRayBetweenPositions); REGISTER_LUA_CFUNC(GetRadarErrorParams); @@ -9011,6 +9013,142 @@ int LuaSyncedRead::GetUnitScriptNames(lua_State* L) return 1; } + +static int TraceRayImpl(lua_State *const L, const float3 &pos, const float3 &dir, const float maxLen, std::string_view type) +{ + if (type != "unit" && type != "feature" && type != "both") + return luaL_error(L, "invalid type '%s', expected 'unit', 'feature', or 'both'", type.data()); + + const bool testUnits = (type == "unit" || type == "both"); + const bool testFeatures = (type == "feature" || type == "both"); + + QuadFieldQuery qfQuery; + quadField.GetQuadsOnRay(qfQuery, pos, dir, maxLen); + + spring::unordered_set testedUnitIDs; + spring::unordered_set testedFeatureIDs; + std::vector > hits; + + for (const int quadIdx : *qfQuery.quads) { + const CQuadField::Quad& quad = quadField.GetQuad(quadIdx); + + if (testUnits) { + for (const auto *unit : quad.units) { + if (!unit->HasCollidableStateBit(CSolidObject::CSTATE_BIT_QUADMAPRAYS)) + continue; + + if (!testedUnitIDs.insert(unit->id).second) + continue; + + if (!LuaUtils::IsUnitInLos(L, unit)) + continue; + + CollisionQuery cq; + if (CCollisionHandler::DetectHit(unit, unit->GetTransformMatrix(true), pos, pos + dir * maxLen, &cq, true)) { + const float len = cq.GetHitPosDist(pos, dir); + if (len > maxLen) // possibly a bug in CCollisionHandler::DetectHit? + continue; + hits.emplace_back(len, unit->id, "unit"); + } + } + } + + if (testFeatures) { + for (const auto *feature : quad.features) { + if (!feature->HasCollidableStateBit(CSolidObject::CSTATE_BIT_QUADMAPRAYS)) + continue; + + if (!testedFeatureIDs.insert(feature->id).second) + continue; + + if (!LuaUtils::IsFeatureVisible(L, feature)) + continue; + + CollisionQuery cq; + if (CCollisionHandler::DetectHit(feature, feature->GetTransformMatrix(true), pos, pos + dir * maxLen, &cq, true)) { + const float len = cq.GetHitPosDist(pos, dir); + if (len > maxLen) + continue; + hits.emplace_back(len, feature->id, "feature"); + } + } + } + } + + std::stable_sort(hits.begin(), hits.end(), [] (const auto& a, const auto& b) { + return std::get<0>(a) < std::get<0>(b); + }); + + lua_createtable(L, hits.size(), 0); + + int num = 0; + for (const auto& [hitLength, objectID, objectType] : hits) { + lua_createtable(L, 3, 0); + + lua_pushnumber(L, hitLength); + lua_rawseti(L, -2, 1); + lua_pushnumber(L, objectID); + lua_rawseti(L, -2, 2); + lua_pushstring(L, objectType); + lua_rawseti(L, -2, 3); + + lua_rawseti(L, -2, ++num); + } + + return 1; +} + +/*** Traces a ray from a position in a direction + * + * @function Spring.TraceRayInDirection + * + * Returns all unit and/or feature hits along a ray, sorted by distance + * from the start position. + * + * @param posX number + * @param posY number + * @param posZ number + * @param dirX number + * @param dirY number + * @param dirZ number + * @param maxLength number + * @param type string Object type to test: `"unit"`, `"feature"`, or `"both"` + * @return table[] hits Array of `{hitLength, objectID, objectType}` entries + */ +int LuaSyncedRead::TraceRayInDirection(lua_State* L) +{ + float3 pos(luaL_checkfloat(L, 1), luaL_checkfloat(L, 2), luaL_checkfloat(L, 3)); + float3 dir(luaL_checkfloat(L, 4), luaL_checkfloat(L, 5), luaL_checkfloat(L, 6)); + const float maxLen = luaL_optfloat(L, 7, 999999.f); + const char* type = luaL_checkstring(L, 8); + return TraceRayImpl(L, pos, dir, maxLen, type); +} + +/*** Traces a ray between two positions + * + * @function Spring.TraceRayBetweenPositions + * + * Checks for unit and/or feature collisions between two positions + * and returns all hits sorted by distance from the start position. + * + * @param startX number + * @param startY number + * @param startZ number + * @param endX number + * @param endY number + * @param endZ number + * @param type string Object type to test: `"unit"`, `"feature"`, or `"both"` + * @return table[] hits Array of `{hitLength, objectID, objectType}` entries + */ +int LuaSyncedRead::TraceRayBetweenPositions(lua_State* L) +{ + float3 start(luaL_checkfloat(L, 1), luaL_checkfloat(L, 2), luaL_checkfloat(L, 3)); + float3 end(luaL_checkfloat(L, 4), luaL_checkfloat(L, 5), luaL_checkfloat(L, 6)); + const char* type = luaL_checkstring(L, 7); + const auto [dir, length] = (end - start).GetNormalized(); + return TraceRayImpl(L, start, dir, length, type); +} + static int TraceRayGroundImpl(lua_State *const L, const float3 &pos, const float3 &dir, const float maxLen, const bool testWater) { const float rayLength = CGround::LineGroundWaterCol(pos, dir, maxLen, testWater, CLuaHandle::GetHandleSynced(L)); diff --git a/rts/Lua/LuaSyncedRead.h b/rts/Lua/LuaSyncedRead.h index 98c632ad9b0..dbec13c0ab7 100644 --- a/rts/Lua/LuaSyncedRead.h +++ b/rts/Lua/LuaSyncedRead.h @@ -302,9 +302,8 @@ class LuaSyncedRead { static int GetRadarErrorParams(lua_State* L); - static int TraceRay(lua_State* L); //TODO: not implemented - static int TraceRayUnits(lua_State* L); //TODO: not implemented - static int TraceRayFeatures(lua_State* L); //TODO: not implemented + static int TraceRayInDirection(lua_State* L); + static int TraceRayBetweenPositions(lua_State* L); static int TraceRayGroundBetweenPositions(lua_State* L); static int TraceRayGroundInDirection(lua_State* L); }; From f1df2dc1874394e9609ccc5a037c2222aa1f702c Mon Sep 17 00:00:00 2001 From: sprunk Date: Thu, 2 Jul 2026 22:25:29 +0200 Subject: [PATCH 26/90] Fix #3019, missing include --- test/other/testMutex.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/test/other/testMutex.cpp b/test/other/testMutex.cpp index 0a84f129e45..b8e3ca5ec68 100644 --- a/test/other/testMutex.cpp +++ b/test/other/testMutex.cpp @@ -14,6 +14,7 @@ #ifndef _WIN32 #include #include + #include #endif #ifdef _WIN32 From a15b7b3a1da6ec8f5025ed25df9efcbe81156237 Mon Sep 17 00:00:00 2001 From: sprunk Date: Sat, 30 May 2026 22:13:32 +0200 Subject: [PATCH 27/90] Fix #2987, edge scrolling threshold --- rts/Game/Camera.cpp | 4 ++-- rts/Game/Camera.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/rts/Game/Camera.cpp b/rts/Game/Camera.cpp index 8c5ea462a4b..921c1b9ffb5 100644 --- a/rts/Game/Camera.cpp +++ b/rts/Game/Camera.cpp @@ -746,8 +746,8 @@ float3 CCamera::GetMoveVectorFromState(bool fromKeyState) const } int2 border; - border.x = std::max(1, windowW * edgeMoveWidth); - border.y = std::max(1, viewH * edgeMoveWidth); + border.x = std::max(1, static_cast (windowW * edgeMoveWidth)); + border.y = std::max(1, static_cast ( viewH * edgeMoveWidth)); float2 move; // must be float, ints don't save the sign in case of 0 and we need it for copysign() diff --git a/rts/Game/Camera.h b/rts/Game/Camera.h index 7901f2218e2..5a114e152f8 100644 --- a/rts/Game/Camera.h +++ b/rts/Game/Camera.h @@ -284,7 +284,7 @@ class CCamera { */ float moveSlowMult; - int edgeMoveWidth; + float edgeMoveWidth; int useInterpolate; bool edgeMoveDynamic; From 2b8528322d8e8ba169d347f48543c48dd8b1bbf2 Mon Sep 17 00:00:00 2001 From: sprunk Date: Sat, 11 Apr 2026 23:14:28 +0200 Subject: [PATCH 28/90] Add type checking to ADD_FOO Lua defs macros --- rts/Lua/LuaDefs.h | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/rts/Lua/LuaDefs.h b/rts/Lua/LuaDefs.h index 091f2bb5f6e..2959e058afd 100644 --- a/rts/Lua/LuaDefs.h +++ b/rts/Lua/LuaDefs.h @@ -60,16 +60,18 @@ namespace { // Requires a "start" address, use ADDRESS() #define ADD_INT(lua, cpp) \ + static_assert(std::is_integral_v, \ + "Lua API break! Add DataElement manually"); \ paramMap[lua] = DataElement(GetDataType(cpp), ADDRESS(cpp) - start) -#define ADD_BOOL(lua, cpp) \ +#define ADD_SIMPLE_DATA(lua, cpp, expected_type) \ + static_assert(std::is_same_v, \ + "Lua API break! Add DataElement manually"); \ paramMap[lua] = DataElement(GetDataType(cpp), ADDRESS(cpp) - start) -#define ADD_FLOAT(lua, cpp) \ - paramMap[lua] = DataElement(GetDataType(cpp), ADDRESS(cpp) - start) - -#define ADD_STRING(lua, cpp) \ - paramMap[lua] = DataElement(GetDataType(cpp), ADDRESS(cpp) - start) +#define ADD_BOOL(lua, cpp) ADD_SIMPLE_DATA(lua, cpp, bool) +#define ADD_FLOAT(lua, cpp) ADD_SIMPLE_DATA(lua, cpp, float) +#define ADD_STRING(lua, cpp) ADD_SIMPLE_DATA(lua, cpp, std::string) #define ADD_FUNCTION(lua, cpp, func) \ paramMap[lua] = DataElement(FUNCTION_TYPE, ADDRESS(cpp) - start, func) From ed68dc1c023ce334669bcfbfb0990eaac3ad6d32 Mon Sep 17 00:00:00 2001 From: sprunk Date: Fri, 1 May 2026 21:37:43 +0200 Subject: [PATCH 29/90] Remove legacy engineoptions * these no longer did anything and just polluted the interface. * also removed example gadgetry using them from basecontent. --- cont/LuaUI/debug.lua | 2 - cont/base/springcontent/CMakeLists.txt | 2 - cont/base/springcontent/EngineOptions.lua | 9 - .../LuaGadgets/Gadgets/README.txt | 1 - .../LuaGadgets/Gadgets/share_delayed.lua | 402 ------------------ .../LuaGadgets/Gadgets/unit_limit_dgun.lua | 155 ------- doc/StartScriptFormat.txt | 2 - .../content/articles/start-script-format.md | 2 - test/validation/prepare.sh | 1 - tools/benchmark/script_benchmark_zwzsg.txt | 1 - tools/scripts/runner.sh | 3 - 11 files changed, 580 deletions(-) delete mode 100644 cont/base/springcontent/LuaGadgets/Gadgets/share_delayed.lua delete mode 100644 cont/base/springcontent/LuaGadgets/Gadgets/unit_limit_dgun.lua diff --git a/cont/LuaUI/debug.lua b/cont/LuaUI/debug.lua index 26cb771850d..f938a8d9889 100644 --- a/cont/LuaUI/debug.lua +++ b/cont/LuaUI/debug.lua @@ -313,8 +313,6 @@ function Debug() print("Game.mapSizeZ = " .. Game.mapSizeZ) print("Game.mapName = " .. Game.mapName) print("Game.modName = " .. Game.modName) - print("Game.limitDGun = " .. tostring(Game.limitDGun)) - print("Game.Game.diminishingMetal = " .. tostring(Game.diminishingMetal)) PrintAllyTeamList() PrintTeamList() diff --git a/cont/base/springcontent/CMakeLists.txt b/cont/base/springcontent/CMakeLists.txt index 95e5769f31e..01509b86128 100644 --- a/cont/base/springcontent/CMakeLists.txt +++ b/cont/base/springcontent/CMakeLists.txt @@ -101,7 +101,6 @@ list(APPEND FILES LuaGadgets/Gadgets/share_levels.lua LuaGadgets/Gadgets/cmd_nocost.lua LuaGadgets/Gadgets/game_end.lua - LuaGadgets/Gadgets/share_delayed.lua LuaGadgets/Gadgets/README.txt LuaGadgets/Gadgets/share_no_builders.lua LuaGadgets/Gadgets/object_statusbars_default.lua @@ -109,7 +108,6 @@ list(APPEND FILES LuaGadgets/Gadgets/share_control.lua LuaGadgets/Gadgets/unit_script.lua LuaGadgets/Gadgets/game_spawn.lua - LuaGadgets/Gadgets/unit_limit_dgun.lua LuaGadgets/system.lua LuaGadgets/actions.lua LuaGadgets/README.txt diff --git a/cont/base/springcontent/EngineOptions.lua b/cont/base/springcontent/EngineOptions.lua index f1b0bfc0de8..23dd42ce902 100644 --- a/cont/base/springcontent/EngineOptions.lua +++ b/cont/base/springcontent/EngineOptions.lua @@ -38,15 +38,6 @@ local options = step = 1, -- quantization is aligned to the def value -- (step <= 0) means that there is no quantization }, - - { - key = 'LimitDgun', - name = 'Limit D-Gun range', - desc = "The commander's D-Gun weapon will be usable only close to the player's starting location", - type = 'bool', - def = false, - }, - { key = 'GhostedBuildings', name = 'Ghosted buildings', diff --git a/cont/base/springcontent/LuaGadgets/Gadgets/README.txt b/cont/base/springcontent/LuaGadgets/Gadgets/README.txt index c859ac704bb..1fa49aa2a23 100644 --- a/cont/base/springcontent/LuaGadgets/Gadgets/README.txt +++ b/cont/base/springcontent/LuaGadgets/Gadgets/README.txt @@ -9,7 +9,6 @@ LuaRules * cmd_nocost.lua * share_control.lua -* share_delayed.lua * share_levels.lua * share_no_builders.lua diff --git a/cont/base/springcontent/LuaGadgets/Gadgets/share_delayed.lua b/cont/base/springcontent/LuaGadgets/Gadgets/share_delayed.lua deleted file mode 100644 index f19b841d48f..00000000000 --- a/cont/base/springcontent/LuaGadgets/Gadgets/share_delayed.lua +++ /dev/null @@ -1,402 +0,0 @@ --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- --- --- file: share_delayed.lua --- brief: delay unit sharing --- author: Dave Rodgers --- --- Copyright (C) 2007. --- Licensed under the terms of the GNU GPL, v2 or later. --- --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- - -function gadget:GetInfo() - return { - name = "SharingDelayed", - desc = "delayed unit sharing", - author = "trepan", - date = "Apr 22, 2007", - license = "GNU GPL, v2 or later", - layer = -4, - enabled = true -- loaded by default? - } -end - --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- --- --- FIXME: (TODO) --- - Delayed resource sharing --- - Visual indicators for units queued to be shared --- - Update unit share times for fallen comrades --- --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- - --- Only active in comm-ends games - -local gameMode = Game.gameMode or 4 - -if (gameMode ~= 1) then - return false -end - - --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- --- --- Proposed Command ID Ranges: --- --- all negative: Engine (build commands) --- 0 - 999: Engine --- 1000 - 9999: Group AI --- 10000 - 19999: LuaUI --- 20000 - 29999: LuaCob --- 30000 - 39999: LuaRules --- - -local CMD_CANCEL_SHARE = 33999 - - --------------------------------------------------------------------------------- --- COMMON --------------------------------------------------------------------------------- -if (gadgetHandler:IsSyncedCode()) then --------------------------------------------------------------------------------- --- SYNCED --------------------------------------------------------------------------------- - -local teams = {} -- teamID = { unitID = shareInfo } -local shares = {} -- unitID = oldTeam -local frames = {} -- unitID = framee - -local enabled = true - -local minDelay = 1 -- minimum delay between shares -local costScale = 0.1 -- add extra cycles based on unit costs - -local cancelShareCmdDesc = { - id = CMD_CANCEL_SHARE, - type = CMDTYPE.ICON, - name = '\255\255\100\100NoShare', - tooltip = 'Cancel the unit transfer', - action = 'cancel_share', -} - - --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- - -local function AllowAction(playerID) - if (playerID ~= 0) then - Spring.SendMessageToPlayer(playerID, "Must be the host player") - return false - end - if (not Spring.IsCheatingEnabled()) then - Spring.SendMessageToPlayer(playerID, "Cheating must be enabled") - return false - end - return true -end - - -local function ChatControl(cmd, line, words, playerID) - if (not AllowAction(playerID)) then - Spring.Echo('delayed sharing is ' .. (enabled and 'enabled' or 'disabled')) - return true - end - if (#words == 0) then - enabled = not enabled - else - enabled = (words[1] == '1') - end - Spring.Echo('delayed sharing is ' .. (enabled and 'enabled' or 'disabled')) - return true -end - - -local function StopShare(cmd, line, words, playerID) - local _,_,_,teamID = Spring.GetPlayerInfo(playerID) - local team = teamID and teams[teamID] or nil - if (team) then - for _,data in pairs(team) do - shares[data.unitID] = nil - frames[data.unitID] = nil - end - teams[teamID] = nil - Spring.Echo('cancelled remaining unit transfers') - else - Spring.Echo('there are no unit transfers to cancel') - end - return true -end - - --------------------------------------------------------------------------------- - -function gadget:Initialize() - gadgetHandler:RegisterCMDID(CMD_CANCEL_SHARE) - _G.shareFrames = frames - - local cmd, help - - cmd = "sharedelay" - help = " [0|1]: delayed unit sharing, useful for comm ends games" - gadgetHandler:AddChatAction(cmd, ChatControl, help) - Script.AddActionFallback(cmd .. ' ', help) - - cmd = "stopshare" - help = ": cancel all queued unit transfers for your team" - gadgetHandler:AddChatAction(cmd, StopShare, help) - Script.AddActionFallback(cmd, help) -end - - -function gadget:Shutdown() - gadgetHandler:RemoveChatAction("sharedelay") - Script.RemoveActionFallback("sharedelay") - - gadgetHandler:RemoveChatAction("stopshare") - Script.RemoveActionFallback("stopshare") - - for _,unitID in ipairs(Spring.GetAllUnits()) do - local cmdDescID = Spring.FindUnitCmdDesc(unitID, CMD_CANCEL_SHARE) - if (cmdDescID) then - Spring.RemoveUnitCmdDesc(unitID, cmdDescID) - end - end -end - - --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- - -local insert = table.insert -local remove = table.remove - - -local function InsertShare(unitID, oldTeam, newTeam, delay) - if (shares[unitID]) then - return -- already active - end - - local shareInfo = { - unitID = unitID, - oldTeam = oldTeam, - newTeam = newTeam, - delay = delay, - } - local team = teams[oldTeam] - - if (team) then - print(team[#team].frame, delay) - shareInfo.frame = team[#team].frame + delay - else - team = {} - teams[oldTeam] = team - shareInfo.frame = Spring.GetGameFrame() + delay - end - - insert(team, shareInfo) - shares[unitID] = oldTeam - frames[unitID] = shareInfo.frame - - Spring.InsertUnitCmdDesc(unitID, 1, cancelShareCmdDesc) -end - - --------------------------------------------------------------------------------- - -local function RecalcDelays(team) - for i = 2, #team do - team[i].frame = team[i - 1].frame + team[i].delay - frames[team[i].unitID] = team[i].frame - end -end - - -local function RemoveShare(unitID) - local oldTeam = shares[unitID] - shares[unitID] = nil - frames[unitID] = nil - - local cmdDescID = Spring.FindUnitCmdDesc(unitID, CMD_CANCEL_SHARE) - if (cmdDescID) then - Spring.RemoveUnitCmdDesc(unitID, cmdDescID) - end - - local team = teams[oldTeam] - if ((oldTeam == nil) or (team == nil)) then - return -- not active - end - - local index - for i, shareInfo in ipairs(team) do - if (shareInfo.unitID == unitID) then - index = i - break - end - end - if (index == nil) then - return -- something is amiss - end - - local shareInfo = team[index] - remove(team, index) - - if (#team <= 0) then - teams[oldTeam] = nil - else - if (index ~= 1) then - RecalcDelays(team) - else - local nowFrame = Spring.GetGameFrame() - if (shareInfo.frame > nowFrame) then - local front = team[1] - front.frame = nowFrame + front.delay - frames[front.unitID] = front.frame - RecalcDelays(team) - end - end - end -end - - --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- - -function gadget:AllowUnitTransfer(unitID, unitDefID, oldTeam, newTeam, capture) - if (capture) then - return true - end - if (not enabled) then - return true - end - - local ud = UnitDefs[unitDefID] - if (not ud) then - return true -- something is borked - end - - -- compute the share delay - local cost = ud.metalCost + (ud.energyCost / 60) - local costDelay = math.floor(cost * costScale) - local shareDelay = minDelay + costDelay - - local team = teams[oldTeam] - if ((team == nil) and (shareDelay <= 0)) then - return true -- share the unit immediately - end - - InsertShare(unitID, oldTeam, newTeam, shareDelay) - - return false -end - - --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- - -function gadget:GameFrame(frameNum) - for _,team in pairs(teams) do - while (team and (#team > 0)) do - local front = team[1] - if (front.frame > frameNum) then - break -- front is not yet ready to be shared - end - - local curTeam = Spring.GetUnitTeam(front.unitID) - if (curTeam and (curTeam == front.oldTeam)) then - -- FIXME: see if newTeam is alive - local tmp = AllowUnitTransfer - AllowUnitTransfer = function() return true end - Spring.TransferUnit(front.unitID, front.newTeam) - AllowUnitTransfer = tmp - end - - RemoveShare(front.unitID) - end - end -end - - --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- - -function gadget:AllowCommand(unitID, unitDefID, unitTeam, - cmdID, cmdParams, cmdOptions) - if (cmdID == CMD_CANCEL_SHARE) then - RemoveShare(unitID) - return false - end - return true -end - - --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- - -function gadget:UnitDestroyed(unitID, unitDefID, unitTeam) - RemoveShare(unitID) -end - - --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- - -function gadget:UnitTaken(unitID) - RemoveShare(unitID) -end - - --------------------------------------------------------------------------------- --- SYNCED --------------------------------------------------------------------------------- -else --------------------------------------------------------------------------------- --- UNSYNCED --------------------------------------------------------------------------------- - -function gadget:Initialize() -end - - -function gadget:Shutdown() -end - - -local GetGameFrame = Spring.GetGameFrame -local GetUnitPosition = Spring.GetUnitPosition -local GetUnitAllyTeam = Spring.GetUnitAllyTeam -local GetLocalAllyTeamID = Spring.GetLocalAllyTeamID -local AddWorldIcon = Spring.AddWorldIcon -local AddWorldText = Spring.AddWorldText - -function gadget:DrawWorld() - local frames = SYNCED.shareFrames - if ((frames == nil) or (snext(frames) == nil)) then - return - end - local nowFrame = GetGameFrame() - local myAllyTeam = GetLocalAllyTeamID() - for unitID, frame in spairs(frames) do - if (GetUnitAllyTeam(unitID) == myAllyTeam) then - local x, y, z = GetUnitPosition(unitID) - if (x) then - local str = string.format('%.1f', (frame - nowFrame) / Game.gameSpeed) - AddWorldIcon(x, y, z, CMD.STOP) - AddWorldText(str, x, y, z) - end - end - end -end - - --------------------------------------------------------------------------------- --- UNSYNCED --------------------------------------------------------------------------------- -end --------------------------------------------------------------------------------- --- COMMON --------------------------------------------------------------------------------- diff --git a/cont/base/springcontent/LuaGadgets/Gadgets/unit_limit_dgun.lua b/cont/base/springcontent/LuaGadgets/Gadgets/unit_limit_dgun.lua deleted file mode 100644 index e5bcf08e67e..00000000000 --- a/cont/base/springcontent/LuaGadgets/Gadgets/unit_limit_dgun.lua +++ /dev/null @@ -1,155 +0,0 @@ --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- --- --- file: unit_dgun_limit.lua --- brief: Re-implements limit dgun in Lua --- author: Andrea Piras --- --- Copyright (C) 2010. --- Licensed under the terms of the GNU GPL, v2 or later. --- --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- - -function gadget:GetInfo() - return { - name = "Limit Dgun", - desc = "Re-implements limit dgun in Lua", - author = "Andrea Piras", - date = "August, 2010", - license = "GNU GPL, v2 or later", - layer = 0, - enabled = true -- loaded by default? - } -end - --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- - -local enabled = tonumber(Spring.GetModOptions().limitdgun) or 0 -if (enabled == 0) then - return false -end - -local dgunMapFraction = 3 -- use the modoption to define it instead? -local dgunRadiusSquared = (Game.mapSizeX*Game.mapSizeZ) / (dgunMapFraction*dgunMapFraction) -local dgunRadius = math.sqrt(dgunRadiusSquared) - -if (not gadgetHandler:IsSyncedCode()) then --begin unsynced section - -local glColor = gl.Color -local glDrawGroundCircle = gl.DrawGroundCircle -local glPushMatrix = gl.PushMatrix -local glPopMatrix = gl.PopMatrix -local glDeleteList = gl.DeleteList -local glCreateList = gl.CreateList -local glCallList = gl.CallList - -local GetSpectatingState = Spring.GetSpectatingState -local GetTeamStartPosition = Spring.GetTeamStartPosition -local GetTeamList = Spring.GetTeamList -local GetTeamStartPosition = Spring.GetTeamStartPosition -local AreTeamsAllied = Spring.AreTeamsAllied -local GetGaiaTeamID = Spring.GetGaiaTeamID -local GetMyTeamID = Spring.GetMyTeamID -local GetTeamInfo = Spring.GetTeamInfo - -local allyTeamVec = Spring.GetAllyTeamList() -local myTeamID = GetMyTeamID() -local gaiaTeamID = GetGaiaTeamID() -local circlesList - -function ReGenerateDisplayList() - if circlesList then - glDeleteList(circlesList) - end - circlesList = glCreateList( function() - for allyIndex,allyTeamID in ipairs(allyTeamVec) do - local teamList = GetTeamList(allyTeamID) - for _,teamID in ipairs(teamList) do - local _,_,isDead = GetTeamInfo(teamID) - if not isDead and teamID ~= gaiaTeamID then - if teamID == myTeamID then - glColor(0.0, 0.0, 1.0, 0.6) -- use blue circles for your range - elseif AreTeamsAllied( teamID, myTeamID ) == true then -- order matters! this tells if he's allied with us, not vice-versa - glColor(1.0, 1.0, 1.0, 0.6) -- use white circles for ally ranges - else - glColor(1.0, 1.0, 0.0, 0.6) -- use yellow circles for enemy ranges - end - local teamX,teamY,teamZ = GetTeamStartPosition(teamID) - if teamX and teamY and teamZ then - glDrawGroundCircle(teamX,teamY,teamZ, dgunRadius, 40) - end - end - end - end - end) -end - -function gadget:PlayerChanged() -- regenerate the display list, update even if it's not for ourself, so we can rid of unnecessary team circles - ReGenerateDisplayList() -end - -function gadget:Initialize() - ReGenerateDisplayList() -end - -function gadget:Shutdown() - glDeleteList(circlesList) -end - -function gadget:DrawWorld() -- paint dgun limit preview range circle - if circlesList then - glPushMatrix() - glCallList(circlesList) - glPopMatrix() - end -end - -else -- begin synced section - -local GetTeamStartPosition = Spring.GetTeamStartPosition -local GetUnitPosition = Spring.GetUnitPosition -local CMD_MANUALFIRE = CMD.MANUALFIRE - -local teamStartPosVec = {} -- cache for team starting pos - --- squared distance between 2 points in 3D cartesian space -function SqDistance(one, two) - dx = one[1] - two[1] - dy = one[2] - two[2] - dz = one[3] - two[3] - return dx*dx + dy*dy + dz*dz -end - -function gadget:AllowCommand(unitID, unitDefID, teamID, cmdID, cmdParams, cmdOptions, cmdTag, synced) - if cmdID ~= CMD_MANUALFIRE then -- pass all non-dgun commands - return true - end - - if not teamStartPosVec[teamID] then -- build cache if not available - local teamX,teamY,teamZ = GetTeamStartPosition(teamID) - if teamX and teamY and teamZ then - teamStartPosVec[teamID] = {teamX,teamY,teamZ} - end - end - local teamStartPos = teamStartPosVec[teamID] - if not teamStartPos then -- wtf - return true - end - - local unitX, unitY, unitZ = GetUnitPosition(unitID) - if not unitX or not unitY or not unitZ then -- wtf - return true - end - local unitPos = {unitX,unitY,unitZ} - - -- restrict the command to the allowed area - if SqDistance(teamStartPos,unitPos) > dgunRadiusSquared then -- unit outside of allowed range, block the command - return false - end - - return true -end - -end -- end synced section diff --git a/doc/StartScriptFormat.txt b/doc/StartScriptFormat.txt index f7ce0a0c8e1..9c13689a6e4 100644 --- a/doc/StartScriptFormat.txt +++ b/doc/StartScriptFormat.txt @@ -137,8 +137,6 @@ StartMetal=1000; StartEnergy=1000; MaxUnits=500; // per team - GameMode=x; // 0 cmdr dead->game continues, 1 cmdr dead->game ends, 2 lineage, 3 openend - LimitDGun=0; // limit dgun to fixed radius around startpos? DisableMapDamage=0; // disable map craters? GhostedBuildings=1; // ghost enemy buildings after losing los on them NoHelperAIs=0; // are GroupAIs and other helper AIs allowed? diff --git a/doc/site/content/articles/start-script-format.md b/doc/site/content/articles/start-script-format.md index 75222119e85..df2300799df 100644 --- a/doc/site/content/articles/start-script-format.md +++ b/doc/site/content/articles/start-script-format.md @@ -143,8 +143,6 @@ draft = false StartMetal=1000; StartEnergy=1000; MaxUnits=500; // per team - GameMode=x; // 0 cmdr dead->game continues, 1 cmdr dead->game ends, 2 lineage, 3 openend - LimitDGun=0; // limit dgun to fixed radius around startpos? DisableMapDamage=0; // disable map craters? GhostedBuildings=1; // ghost enemy buildings after losing los on them NoHelperAIs=0; // are GroupAIs and other helper AIs allowed? diff --git a/test/validation/prepare.sh b/test/validation/prepare.sh index e62c45dd675..1fa1e4adf45 100755 --- a/test/validation/prepare.sh +++ b/test/validation/prepare.sh @@ -33,7 +33,6 @@ cat <game continues, 1 cmdr dead->game ends - LimitDgun=1; // limit dgun to fixed radius around startpos? - DiminishingMMs=0; // diminish metal maker's metal production for every new one of them? DisableMapDamage=0; // disable map craters? GhostedBuildings=1; // ghost enemy buildings after losing los on them From 615d257c1293af91df4c0aff1b563c1eb9f0d248 Mon Sep 17 00:00:00 2001 From: lostsquirrel1 Date: Mon, 6 Jul 2026 12:36:20 +0100 Subject: [PATCH 30/90] fix desync --- rts/Sim/Path/QTPFS/PathManager.cpp | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/rts/Sim/Path/QTPFS/PathManager.cpp b/rts/Sim/Path/QTPFS/PathManager.cpp index 1aa5e9a36c5..247a277049c 100644 --- a/rts/Sim/Path/QTPFS/PathManager.cpp +++ b/rts/Sim/Path/QTPFS/PathManager.cpp @@ -933,6 +933,15 @@ bool QTPFS::PathManager::InitializeSearch(QTPFS::entity searchEntity) { assert((registry.any_of(pathEntity))); IPath* path = GetPath(pathEntity); assert(path->GetPathType() == pathType); + + // Somehow units can get wiped without triggering a delete. This is a catch for that until the + // cause can be found and resolved. + const CSolidObject* owner = path->GetOwner(); + if (owner != nullptr) { + if (owner->objectUsable == false) + return false; + } + search->Initialize(&nodeLayer, path->GetSourcePoint(), path->GetGoalPosition(), path->GetOwner()); path->SetHash(search->GetHash()); path->SetVirtualHash(search->GetPartialSearchHash()); @@ -1122,20 +1131,13 @@ bool QTPFS::PathManager::ExecuteSearch( int currentThread = ThreadPool::GetThreadNum(); assert(search != nullptr); + assert(search->initialized); // temp-path might have been removed already via // DeletePath before we got a chance to process it if (path == nullptr) return false; - // Somehow units can get wiped without triggering a delete. This is a catch for that until the - // cause can be found and resolved. - const CSolidObject* owner = path->GetOwner(); - if (owner != nullptr) { - if (owner->objectUsable == false) - return false; - } - assert(path->GetID() == search->GetID()); bool forceFullPath = false; From c420138585db7d744be856cbb381d32dc4683576 Mon Sep 17 00:00:00 2001 From: PtaQQ Date: Tue, 23 Jun 2026 20:33:13 +0200 Subject: [PATCH 31/90] SpringMath: add Catmull-Rom bicubic interpolation helpers CatmullRom does C1-continuous cubic interpolation through two samples given their outer neighbours; InterpolateBicubic applies it over a 4x4 patch. Used by the smooth mesh sampler for a crease-free surface. Co-Authored-By: Claude Opus 4.8 (1M context) --- rts/System/SpringMath.h | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/rts/System/SpringMath.h b/rts/System/SpringMath.h index 2d3af6ffe9b..f49871c778c 100644 --- a/rts/System/SpringMath.h +++ b/rts/System/SpringMath.h @@ -175,6 +175,26 @@ template constexpr T mixRotation(T v1, T v2, T2 a) { template constexpr T Blend(const T v1, const T v2, const float a) { return mix(v1, v2, a); } +// Catmull-Rom cubic interpolation through p1..p2, with p0/p3 the outer neighbours, t in [0,1]. +// C1-continuous (matching gradients across segment borders), unlike linear mix. +constexpr float CatmullRom(float p0, float p1, float p2, float p3, float t) +{ + return p1 + 0.5f * t * ((p2 - p0) + t * ((2.0f * p0 - 5.0f * p1 + 4.0f * p2 - p3) + t * (3.0f * (p1 - p2) + p3 - p0))); +} + +// Bicubic Catmull-Rom over a 4x4 patch of samples p[row][col]; dx/dy are the +// fractional offsets in [0,1] from the inner sample p[1][1] toward p[2][2]. +inline float InterpolateBicubic(const float p[4][4], float dx, float dy) +{ + const float cols[4] = { + CatmullRom(p[0][0], p[0][1], p[0][2], p[0][3], dx), + CatmullRom(p[1][0], p[1][1], p[1][2], p[1][3], dx), + CatmullRom(p[2][0], p[2][1], p[2][2], p[2][3], dx), + CatmullRom(p[3][0], p[3][1], p[3][2], p[3][3], dx), + }; + return CatmullRom(cols[0], cols[1], cols[2], cols[3], dy); +} + int Round(const float f) _const _warn_unused_result; template constexpr T Square(const T x) { return x*x; } From 6eecb24c143941337574962ccfea453a776f100a Mon Sep 17 00:00:00 2001 From: rlcevg <514675+rlcevg@users.noreply.github.com> Date: Sat, 4 Jul 2026 22:19:50 +0300 Subject: [PATCH 32/90] Update BARb 1.6.26 --- AI/Skirmish/BARb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AI/Skirmish/BARb b/AI/Skirmish/BARb index 0ef36267633..9875f1b332a 160000 --- a/AI/Skirmish/BARb +++ b/AI/Skirmish/BARb @@ -1 +1 @@ -Subproject commit 0ef36267633d6c1b2f6408a8d8a59fff38745dc3 +Subproject commit 9875f1b332aa037c7a9f6e8bea1f8ecd9f289096 From 62206aa0d36422b1170cca84f032750126651e71 Mon Sep 17 00:00:00 2001 From: Luis Valencia <244623113+L01sValencia@users.noreply.github.com> Date: Wed, 8 Jul 2026 17:44:16 -0600 Subject: [PATCH 33/90] fix: links to other getting-started pages --- .../getting-started/first-steps-with-the-engine.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/doc/site/content/docs/guides/getting-started/first-steps-with-the-engine.md b/doc/site/content/docs/guides/getting-started/first-steps-with-the-engine.md index 2f023a572ac..a30c72ad182 100644 --- a/doc/site/content/docs/guides/getting-started/first-steps-with-the-engine.md +++ b/doc/site/content/docs/guides/getting-started/first-steps-with-the-engine.md @@ -302,17 +302,17 @@ Two games are particularly suited for newcomers: 4. Edit Lua files and retest Changes to Lua code take effect on the next game start. For workflow tips, -see the [Lua Language Server guide](lua-language-server/) for setting up IDE +see the [Lua Language Server guide](lua-language-server.md) for setting up IDE autocompletion. ## Next Steps Once you have the engine running with a game and map: -- Learn about [VFS basics](vfs-basics/) to understand how the engine loads content -- Read about [Widgets and Gadgets](widgets-and-gadgets/) to understand Lua scripting -- Explore [basecontent](basecontent/) for the default scripts provided by the engine -- Check out [Unit Types Basics](unit-types-basics/) to define your first units +- Learn about [VFS basics](vfs-basics.md) to understand how the engine loads content +- Read about [Widgets and Gadgets](widgets-and-gadgets.md) to understand Lua scripting +- Explore [basecontent](basecontent.md) for the default scripts provided by the engine +- Check out [Unit Types Basics](unit-types-basics.md) to define your first units For API reference while developing, see the [Lua API documentation](/docs/lua-api/). @@ -340,4 +340,4 @@ error, see the [Write Directory](#write-directory) section above. Quick fixes: When in doubt, the engine sources contain the authoritative behavior. Search the [`rts/System/FileSystem/`](https://github.com/beyond-all-reason/RecoilEngine/tree/master/rts/System/FileSystem) -directory for data directory handling code. \ No newline at end of file +directory for data directory handling code. From 2d4b031d19583bfcdfe3d01964f0848e0e91f1d8 Mon Sep 17 00:00:00 2001 From: Tom J Nowell Date: Fri, 10 Jul 2026 00:55:39 +0100 Subject: [PATCH 34/90] Fix LuaSocket VFS mode error in LuaMenu --- rts/Lua/LuaHandle.cpp | 26 ++++++++++++++++++++++++++ rts/Lua/LuaHandle.h | 1 + rts/Lua/LuaMenu.cpp | 17 ----------------- rts/Lua/LuaMenu.h | 1 - rts/Lua/LuaUI.cpp | 20 -------------------- rts/Lua/LuaUI.h | 1 - 6 files changed, 27 insertions(+), 39 deletions(-) diff --git a/rts/Lua/LuaHandle.cpp b/rts/Lua/LuaHandle.cpp index 650a810499d..6ae29a6f293 100644 --- a/rts/Lua/LuaHandle.cpp +++ b/rts/Lua/LuaHandle.cpp @@ -41,6 +41,8 @@ #include "Sim/Units/UnitDef.h" #include "Sim/Weapons/Weapon.h" #include "Sim/Weapons/WeaponDef.h" +#include "System/FileSystem/FileHandler.h" +#include "System/FileSystem/VFSModes.h" #include "System/creg/SerializeLuaState.h" #include "System/Config/ConfigHandler.h" #include "System/EventHandler.h" @@ -56,6 +58,8 @@ #include "LuaInclude.h" +#include "lib/luasocket/src/luasocket.h" + #include #include #include @@ -549,6 +553,28 @@ bool CLuaHandle::LoadCode(lua_State* L, std::string code, const string& debug) } +void CLuaHandle::InitLuaSocket(lua_State* L) +{ + RECOIL_DETAILED_TRACY_ZONE; + + const std::string filename = "LuaSocket/socket.lua"; + CFileHandler f(filename, SPRING_VFS_BASE); + if (!f.FileExists()) { + LOG_L(L_ERROR, "Error loading %s (file does not exist)", filename.c_str()); + return; + } + + LUA_OPEN_LIB(L, luaopen_socket_core); + + std::string code; + if (f.LoadStringData(code)) { + LoadCode(L, std::move(code), filename); + } else { + LOG_L(L_ERROR, "Error loading %s", filename.c_str()); + } +} + + int CLuaHandle::LoadStringData(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; diff --git a/rts/Lua/LuaHandle.h b/rts/Lua/LuaHandle.h index 5256825a2b6..97ddf4323c9 100644 --- a/rts/Lua/LuaHandle.h +++ b/rts/Lua/LuaHandle.h @@ -329,6 +329,7 @@ class CLuaHandle : public CEventClient bool AddBasicCalls(lua_State* L); bool AddCommonModules(lua_State* L); bool LoadCode(lua_State* L, std::string code, const std::string& debug); + void InitLuaSocket(lua_State* L); static bool AddEntriesToTable(lua_State* L, const char* name, bool (*entriesFunc)(lua_State*)); /// returns error code and sets traceback on error diff --git a/rts/Lua/LuaMenu.cpp b/rts/Lua/LuaMenu.cpp index bafbc08be23..bce52197ec8 100644 --- a/rts/Lua/LuaMenu.cpp +++ b/rts/Lua/LuaMenu.cpp @@ -24,7 +24,6 @@ #include "System/FileSystem/FileHandler.h" #include "System/FileSystem/FileSystem.h" #include "System/Threading/SpringThreading.h" -#include "lib/luasocket/src/luasocket.h" #include "LuaUI.h" #include "System/Misc/TracyDefs.h" @@ -134,22 +133,6 @@ string CLuaMenu::LoadFile(const string& name) const } -void CLuaMenu::InitLuaSocket(lua_State* L) { - RECOIL_DETAILED_TRACY_ZONE; - std::string code; - std::string filename = "socket.lua"; - CFileHandler f(filename); - - LUA_OPEN_LIB(L, luaopen_socket_core); - - if (f.LoadStringData(code)) { - LoadCode(L, std::move(code), filename); - } else { - LOG_L(L_ERROR, "Error loading %s", filename.c_str()); - } -} - - bool CLuaMenu::RemoveSomeOpenGLFunctions(lua_State* L) { // remove some spring opengl functions that don't work preloading diff --git a/rts/Lua/LuaMenu.h b/rts/Lua/LuaMenu.h index 0c65859e90d..aa7a990475e 100644 --- a/rts/Lua/LuaMenu.h +++ b/rts/Lua/LuaMenu.h @@ -61,7 +61,6 @@ class CLuaMenu : public CLuaHandle static bool LoadUnsyncedReadFunctions(lua_State* L); static bool RemoveSomeOpenGLFunctions(lua_State* L); static bool PushGameVersion(lua_State* L); - void InitLuaSocket(lua_State* L); protected: QueuedAction queuedAction; }; diff --git a/rts/Lua/LuaUI.cpp b/rts/Lua/LuaUI.cpp index 01204219fa4..4b768f39f53 100644 --- a/rts/Lua/LuaUI.cpp +++ b/rts/Lua/LuaUI.cpp @@ -40,7 +40,6 @@ #include "System/Config/ConfigHandler.h" #include "System/StringUtil.h" #include "System/Threading/SpringThreading.h" -#include "lib/luasocket/src/luasocket.h" #include @@ -207,25 +206,6 @@ GetWatchDef(Explosion) SetWatchDef(Explosion) -void CLuaUI::InitLuaSocket(lua_State* L) { - std::string code; - std::string filename = "LuaSocket/socket.lua"; - CFileHandler f(filename, SPRING_VFS_BASE); - - if (!f.FileExists()) { - LOG_L(L_ERROR, "Error loading %s (file does not exist)", filename.c_str()); - return; - } - - LUA_OPEN_LIB(L, luaopen_socket_core); - - if (f.LoadStringData(code)) { - LoadCode(L, std::move(code), filename); - } else { - LOG_L(L_ERROR, "Error loading %s", filename.c_str()); - } -} - string CLuaUI::LoadFile(const string& name, const std::string& mode) const { CFileHandler f(name, mode); diff --git a/rts/Lua/LuaUI.h b/rts/Lua/LuaUI.h index 6ee9b2d1f73..dfe2b9ab2a4 100644 --- a/rts/Lua/LuaUI.h +++ b/rts/Lua/LuaUI.h @@ -83,7 +83,6 @@ class CLuaUI : public CLuaHandle string LoadFile(const string& name, const std::string& mode) const; bool LoadCFunctions(lua_State* L); - void InitLuaSocket(lua_State* L); bool BuildCmdDescTable(lua_State* L, const vector& cmds); bool GetLuaIntMap(lua_State* L, int index, spring::unordered_map& intList); From 21b121c2fe7f969eb6a10ccde4df8f2d8c472a7a Mon Sep 17 00:00:00 2001 From: Luis Valencia <244623113+L01sValencia@users.noreply.github.com> Date: Thu, 9 Jul 2026 18:25:54 -0600 Subject: [PATCH 35/90] Fix various typos in site articles (#3090) --- .../guides/getting-started/getting-started-with-rmlui.md | 6 +++--- .../docs/guides/getting-started/widgets-and-gadgets.md | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/doc/site/content/docs/guides/getting-started/getting-started-with-rmlui.md b/doc/site/content/docs/guides/getting-started/getting-started-with-rmlui.md index b99aa69f3f7..2d21709bd9a 100644 --- a/doc/site/content/docs/guides/getting-started/getting-started-with-rmlui.md +++ b/doc/site/content/docs/guides/getting-started/getting-started-with-rmlui.md @@ -5,9 +5,9 @@ draft = false author = "Slashscreen" +++ -RmlUi is a UI framework that is defined using a HTML/CSS style workflow (using Lua instead of JS) intended to simplify UI development especially for those already familiar with web development. It is designed for interactive applications, and so is reactive by default. You can learn more about it on the [RmlUI website] and [differences in the Recoil version here](#differences-between-upstream-rmlui-and-rmlui-in-recoil). +RmlUi is a UI framework that is defined using a HTML/CSS style workflow (using Lua instead of JS) intended to simplify UI development especially for those already familiar with web development. It is designed for interactive applications, and so is reactive by default. You can learn more about it on the [RmlUi website] and [differences in the Recoil version here](#differences-between-upstream-rmlui-and-rmlui-in-recoil). -## How does RmlUI Work? +## How does RmlUi Work? To get started, it's important to learn a few key concepts. - Context: This is a bundle of documents and data models. @@ -416,7 +416,7 @@ document = widget.rmlContext:LoadDocument("document.rml", document_table) - The Beyond All Reason devs prefer to use one shared context for all rmlui widgets. -### Differences between upstream RmlUI and RmlUI in Recoil +### Differences between upstream RmlUi and RmlUi in Recoil - The SVG element allows either a filepath or raw SVG data in the src attribute, allowing for inline svg to be used (this may change to svg being supported between the opening and closing tag when implemented upstream) - An additional element `````` is available which allows for textures loaded in Recoil to be used, this behaves the same as an `````` element except the src attribute takes a [texture reference]({{% ref "articles/texture-reference-strings" %}}) diff --git a/doc/site/content/docs/guides/getting-started/widgets-and-gadgets.md b/doc/site/content/docs/guides/getting-started/widgets-and-gadgets.md index 216f9e21f72..6200e2f24b0 100644 --- a/doc/site/content/docs/guides/getting-started/widgets-and-gadgets.md +++ b/doc/site/content/docs/guides/getting-started/widgets-and-gadgets.md @@ -10,7 +10,7 @@ Before we get to Widgets and Gadgets we will start with an overview of some key - Synced mode is the environment that affects game simulation e.g ordering units around. Synced commands are distributed and run by all players in the game to ensure the simulation remains synced. - Unsynced mode is the players own environment, functionality here could for example enhance controls, display helpful information. -When in Unsynced mode LuaIntro, LuaUi, LuaRules and LuaGaia provide read access to synced but only LuaRules and LuaGaia have full access to simulation information (all players units etc.). In LuaIntro and LuaUi read access to synced is scoped to just what that player can see i.e. observing LoS & radar ranges. +When in Unsynced mode LuaIntro, LuaUI, LuaRules and LuaGaia provide read access to synced but only LuaRules and LuaGaia have full access to simulation information (all players units etc.). In LuaIntro and LuaUI read access to synced is scoped to just what that player can see i.e. observing LoS & radar ranges. ### Key Areas - LuaRules - Generally home to lower level customisations that affect unit behavior or overall game operation @@ -29,7 +29,7 @@ Widgets and Gadgets are concepts that have been adopted by several games using R Gadgets typically being lower level game logic, unit behaviour functionality that defines your game and should be present for all players, they are typically only found in LuaRules and LuaGaia and are often included using VFS.ZIP_ONLY so as not to be easily overridden by end users (your packaged version takes priority). -Widgets usually involve improving UX or showing helpful UI interfaces, and are often considered things that users can turn on/off in the game to suit their needs, be it through settings or a widget manager UI. They are usually specific to LuaIntro, LuaMenu and LuaUi. +Widgets usually involve improving UX or showing helpful UI interfaces, and are often considered things that users can turn on/off in the game to suit their needs, be it through settings or a widget manager UI. They are usually specific to LuaIntro, LuaMenu and LuaUI. To manage the invocation of Widgets & Gadgets a "**handler**" is setup in the Lua entrypoint. For environments with synced and unsyned entry points these typically use the same handler which calls the same widgets/gadgets but use a function the he handler like IsSyncedCode to check if they are being run in synced mode or unsynced mode and running the appropriate section of code. @@ -85,7 +85,7 @@ end ``` ## Handlers -Handlers are the code that runs in the entry point to load in addons/widgets/gadgets and distribute callins to them, and example implementation of a handler is included in the [Recoil base content](https://github.com/beyond-all-reason/RecoilEngine/blob/master/cont/base/springcontent/LuaHandler/handler.lua) for LuaIntro, LuaRules and LuaUi, and will likely do well for a simple game, although many games eventually choose to make their own handlers. This means for LuaIntro and LuaUi you can start creating widgets without worrying about handlers in the luaintro/widgets or luaui/widgets folders, and for creating gadgets for LuaRules in luarules/gadgets folder. +Handlers are the code that runs in the entry point to load in addons/widgets/gadgets and distribute callins to them, and example implementation of a handler is included in the [Recoil base content](https://github.com/beyond-all-reason/RecoilEngine/blob/master/cont/base/springcontent/LuaHandler/handler.lua) for LuaIntro, LuaRules and LuaUI, and will likely do well for a simple game, although many games eventually choose to make their own handlers. This means for LuaIntro and LuaUI you can start creating widgets without worrying about handlers in the luaintro/widgets or luaui/widgets folders, and for creating gadgets for LuaRules in luarules/gadgets folder. Below is a very simplified pseudocode example of a handler is below to illustrate adding a gadget and forwarding callins to gadgets. ```lua From 2ccc5c0d6af669f8ef525c63f8e0cd8cb4c6583e Mon Sep 17 00:00:00 2001 From: sprunk Date: Fri, 8 May 2026 02:42:44 +0200 Subject: [PATCH 36/90] Dump state handles resource packs Also now prints max storage. --- rts/System/Sync/DumpState.cpp | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/rts/System/Sync/DumpState.cpp b/rts/System/Sync/DumpState.cpp index 86ad1ea2788..0293f909105 100644 --- a/rts/System/Sync/DumpState.cpp +++ b/rts/System/Sync/DumpState.cpp @@ -687,14 +687,16 @@ void DumpState(int newMinFrameNum, int newMaxFrameNum, int newFramePeriod, std:: const CTeam* t = teamHandler.Team(a); file << "\t\tteamID: " << t->teamNum << " (controller: " << t->GetControllerName() << ")\n"; - file << "\t\t\tmetal: " << TapFloats(t->res.metal); - file << "\t\t\tenergy: " << TapFloats(t->res.energy); - file << "\t\t\tmetalPull: " << TapFloats(t->resPull.metal); - file << "\t\t\tenergyPull: " << TapFloats(t->resPull.energy); - file << "\t\t\tmetalIncome: " << TapFloats(t->resIncome.metal); - file << "\t\t\tenergyIncome: " << TapFloats(t->resIncome.energy); - file << "\t\t\tmetalExpense: " << TapFloats(t->resExpense.metal); - file << "\t\t\tenergyExpense: " << TapFloats(t->resExpense.energy); + for (const auto &[resourceID, value] : std::views::enumerate(t->res)) + file << "\t\t\tstored[" << resourceID << "]: " << TapFloats(value); + for (const auto &[resourceID, value] : std::views::enumerate(t->resStorage)) + file << "\t\t\tmaxStorage[" << resourceID << "]: " << TapFloats(value); + for (const auto &[resourceID, value] : std::views::enumerate(t->resPull)) + file << "\t\t\tpull[" << resourceID << "]: " << TapFloats(value); + for (const auto &[resourceID, value] : std::views::enumerate(t->resIncome)) + file << "\t\t\tincome[" << resourceID << "]: " << TapFloats(value); + for (const auto &[resourceID, value] : std::views::enumerate(t->resExpense)) + file << "\t\t\texpense[" << resourceID << "]: " << TapFloats(value); } #endif From 74408d1ddc1939cb8c87ec3678080817f9697053 Mon Sep 17 00:00:00 2001 From: Tom J Nowell Date: Fri, 10 Jul 2026 13:49:04 +0100 Subject: [PATCH 37/90] doc/site: rename 7Z_VERSION env var so mise/Tera can parse it mise 2026.7.5 (2026-07-09) tightened Tera template parsing and now rejects {{env.7Z_VERSION}} with 'Found integer but expected identifier' - Tera identifiers cannot start with a digit. Rename the env var to SEVENZIP_VERSION across the workflow and both mise configs; the value is unchanged. --- .github/workflows/publish-site.yml | 2 +- doc/site/mise.ci.toml | 2 +- doc/site/mise.toml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish-site.yml b/.github/workflows/publish-site.yml index efac668ec89..94a959d9893 100644 --- a/.github/workflows/publish-site.yml +++ b/.github/workflows/publish-site.yml @@ -15,7 +15,7 @@ env: LUA_DOC_EXTRACTOR_VERSION: "3" EMMYLUA_DOC_CLI_VERSION: "0.8.2" JQ_VERSION: "1.8.0" - 7Z_VERSION: "24.09" + SEVENZIP_VERSION: "24.09" RUBY_VERSION: "3.3" MISE_ENV: "ci" jobs: diff --git a/doc/site/mise.ci.toml b/doc/site/mise.ci.toml index 50f2d98fccb..f1d504be9db 100644 --- a/doc/site/mise.ci.toml +++ b/doc/site/mise.ci.toml @@ -6,6 +6,6 @@ hugo = "{{env.HUGO_VERSION}}" "cargo:emmylua_doc_cli" = "{{env.EMMYLUA_DOC_CLI_VERSION}}" "aqua:jqlang/jq" = "{{env.JQ_VERSION}}" - "aqua:ip7z/7zip" = "{{env.7Z_VERSION}}" + "aqua:ip7z/7zip" = "{{env.SEVENZIP_VERSION}}" "npm:lua-doc-extractor" = "{{env.LUA_DOC_EXTRACTOR_VERSION}}" "ruby" = "{{env.RUBY_VERSION}}" diff --git a/doc/site/mise.toml b/doc/site/mise.toml index 8604af17fac..3eac83faf22 100644 --- a/doc/site/mise.toml +++ b/doc/site/mise.toml @@ -8,7 +8,7 @@ EMMYLUA_DOC_CLI_VERSION = "0.8.2" # lua_pages lua_library lua_check LUA_LANGUAGE_SERVER_VERSION = "3.15.0" # lua_check JQ_VERSION = "1.8.0" # binary_pages - 7Z_VERSION = "24.09" # binary_pages + SEVENZIP_VERSION = "24.09" # binary_pages RUBY_VERSION = "3.3" # lua_pages LUA_DOC_EXTRACTOR_SOURCE_REF = "https://github.com/beyond-all-reason/RecoilEngine/blob/master" RECOIL_LUA_LIBRARY_DIR = "rts/Lua/library" @@ -51,7 +51,7 @@ depends = ["latest_release_data"] description = "Generate Docs from Recoil binary" tools."aqua:jqlang/jq" = "{{env.JQ_VERSION}}" - tools."aqua:ip7z/7zip" = "{{env.7Z_VERSION}}" + tools."aqua:ip7z/7zip" = "{{env.SEVENZIP_VERSION}}" run = ''' download_url=$(jq -r '.assets[] | select(.name | contains("amd64-linux.7z")).browser_download_url' {{vars.latest_release_data}}) From 0ea0be0a5d160827d342a364eb2212a3e2d5ee40 Mon Sep 17 00:00:00 2001 From: Tom J Nowell Date: Fri, 10 Jul 2026 17:18:35 +0100 Subject: [PATCH 38/90] System: move float3/float4/Matrix44f str() out of line str() was defined inline in the headers, which forced onto every translation unit that includes float3.h - i.e. most of the engine, plus vendored targets (lua, luasocket) that pull it in transitively. That is a lot of compile-time cost for a debug-only helper, and it means any target touching float3.h must have the formatting library's headers available. Move the three str() bodies into their .cpp files and drop from the headers (float3.h keeps for the declaration). Now only the three math .cpp files need . Add the direct include to the two other live std::format users that were relying on the transitive pull (UnsyncedGameCommands.cpp, DumpHistory.cpp). --- rts/Game/UnsyncedGameCommands.cpp | 1 + rts/System/Matrix44f.cpp | 8 ++++++++ rts/System/Matrix44f.h | 6 +----- rts/System/Sync/DumpHistory.cpp | 2 ++ rts/System/float3.cpp | 6 ++++++ rts/System/float3.h | 6 ++---- rts/System/float4.cpp | 7 +++++++ rts/System/float4.h | 4 +--- 8 files changed, 28 insertions(+), 12 deletions(-) diff --git a/rts/Game/UnsyncedGameCommands.cpp b/rts/Game/UnsyncedGameCommands.cpp index d5b80f5f44f..c49edc0a6a7 100644 --- a/rts/Game/UnsyncedGameCommands.cpp +++ b/rts/Game/UnsyncedGameCommands.cpp @@ -1,5 +1,6 @@ /* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ #include +#include #include #include diff --git a/rts/System/Matrix44f.cpp b/rts/System/Matrix44f.cpp index 6368561aca2..4c1328880dd 100644 --- a/rts/System/Matrix44f.cpp +++ b/rts/System/Matrix44f.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include "System/simd_compat.h" @@ -18,6 +19,13 @@ CR_BIND(CMatrix44f, ) CR_REG_METADATA(CMatrix44f, CR_MEMBER(m)) static_assert(alignof(CMatrix44f) == 64); + +std::string CMatrix44f::str() const +{ + return std::format( + "m44(\n{:.3f} {:.3f} {:.3f} {:.3f}\n{:.3f} {:.3f} {:.3f} {:.3f}\n{:.3f} {:.3f} {:.3f} {:.3f}\n{:.3f} {:.3f} {:.3f} {:.3f})", + m[0], m[4], m[8], m[12], m[1], m[5], m[9], m[13], m[2], m[6], m[10], m[14], m[3], m[7], m[11], m[15]); +} CMatrix44f::CMatrix44f(const CMatrix44f& mat) { memcpy(&m[0], &mat.m[0], sizeof(CMatrix44f)); diff --git a/rts/System/Matrix44f.h b/rts/System/Matrix44f.h index bd5e6d1fc5b..f8061e659d7 100644 --- a/rts/System/Matrix44f.h +++ b/rts/System/Matrix44f.h @@ -176,11 +176,7 @@ class CMatrix44f float4 col[4]; }; - std::string str() const { - return std::format( - "m44(\n{:.3f} {:.3f} {:.3f} {:.3f}\n{:.3f} {:.3f} {:.3f} {:.3f}\n{:.3f} {:.3f} {:.3f} {:.3f}\n{:.3f} {:.3f} {:.3f} {:.3f})", - m[0], m[4], m[8], m[12], m[1], m[5], m[9], m[13], m[2], m[6], m[10], m[14], m[3], m[7], m[11], m[15]); - } + std::string str() const; }; diff --git a/rts/System/Sync/DumpHistory.cpp b/rts/System/Sync/DumpHistory.cpp index 8280888df87..724352a6855 100644 --- a/rts/System/Sync/DumpHistory.cpp +++ b/rts/System/Sync/DumpHistory.cpp @@ -1,5 +1,7 @@ /* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ +#include + #include "DumpHistory.h" #include "SyncChecker.h" diff --git a/rts/System/float3.cpp b/rts/System/float3.cpp index 370f53e32a8..6189708d086 100644 --- a/rts/System/float3.cpp +++ b/rts/System/float3.cpp @@ -4,6 +4,7 @@ #include #include +#include #include "System/creg/creg_cond.h" #include "System/SpringMath.h" @@ -15,6 +16,11 @@ CR_REG_METADATA(float3, (CR_MEMBER(x), CR_MEMBER(y), CR_MEMBER(z))) float float3::maxxpos = -1.0f; float float3::maxzpos = -1.0f; +std::string float3::str() const +{ + return std::format("float3({:.3f}, {:.3f}, {:.3f})", x, y, z); +} + float3 float3::PickNonParallel() const { // https://math.stackexchange.com/questions/3122010/how-to-deterministically-pick-a-vector-that-is-guaranteed-to-be-non-parallel-to diff --git a/rts/System/float3.h b/rts/System/float3.h index bf47dd90c2a..e827f2c0b73 100644 --- a/rts/System/float3.h +++ b/rts/System/float3.h @@ -6,7 +6,7 @@ #include #include #include -#include +#include #include "System/BranchPrediction.h" #include "System/creg/creg_cond.h" @@ -838,9 +838,7 @@ class float3 static constexpr float cmp_eps() { return 1e-04f; } static constexpr float nrm_eps() { return 1e-12f; } - std::string str() const { - return std::format("float3({:.3f}, {:.3f}, {:.3f})", x, y, z); - } + std::string str() const; /** * @brief max x pos diff --git a/rts/System/float4.cpp b/rts/System/float4.cpp index 8fcdfb597b6..d6bf6971115 100644 --- a/rts/System/float4.cpp +++ b/rts/System/float4.cpp @@ -4,9 +4,16 @@ #include "System/creg/creg_cond.h" #include "System/SpringMath.h" +#include + CR_BIND(float4, ) CR_REG_METADATA(float4, (CR_MEMBER(x), CR_MEMBER(y), CR_MEMBER(z), CR_MEMBER(w))) +std::string float4::str() const +{ + return std::format("float4({:.3f}, {:.3f}, {:.3f}, {:.3f})", x, y, z, w); +} + // ensure that we use a continuous block of memory // (required for passing to gl functions) static_assert(sizeof(float4) == 4 * sizeof(float), ""); diff --git a/rts/System/float4.h b/rts/System/float4.h index 347f0d2e822..7381701184b 100644 --- a/rts/System/float4.h +++ b/rts/System/float4.h @@ -108,9 +108,7 @@ struct float4 : public float3 return (x * f.x) + (y * f.y) + (z * f.z) + (w * f.w); } - std::string str() const { - return std::format("float4({:.3f}, {:.3f}, {:.3f}, {:.3f})", x, y, z, w); - } + std::string str() const; /// Allows implicit conversion to float* (for passing to gl functions) From c5cb63af37cd8931bbbc2e1e915650c3dc3bf27a Mon Sep 17 00:00:00 2001 From: Alexander Heistermann Date: Fri, 10 Jul 2026 14:46:53 -0500 Subject: [PATCH 39/90] Remove Java AI bindings (#3059) Unused maintenance burden --- AI/Interfaces/Java/CMakeLists.txt | 661 --------- AI/Interfaces/Java/VERSION | 1 - AI/Interfaces/Java/bin/ant.properties | 21 - AI/Interfaces/Java/bin/build.xml | 258 ---- AI/Interfaces/Java/bin/jni_wrappCallback.awk | 573 -------- AI/Interfaces/Java/bin/jni_wrappCommands.awk | 192 --- AI/Interfaces/Java/bin/jni_wrappEvents.awk | 570 -------- ...native_createCallbackFPInitializations.awk | 53 - .../native_createCallbackFunctionImpls.awk | 124 -- .../Java/bin/native_wrappCallback.awk | 290 ---- .../Java/bin/native_wrappCommands.awk | 384 ----- AI/Interfaces/Java/data/InterfaceInfo.lua | 51 - AI/Interfaces/Java/data/jvm.properties | 86 -- AI/Interfaces/Java/pom.xml | 49 - .../src/main/java/com/springrts/ai/Util.java | 55 - .../Java/src/main/native/InterfaceDefines.h | 23 - .../Java/src/main/native/InterfaceExport.c | 157 --- .../Java/src/main/native/InterfaceExport.h | 61 - .../Java/src/main/native/JavaBridge.c | 1219 ---------------- .../Java/src/main/native/JavaBridge.h | 73 - AI/Interfaces/Java/src/main/native/JniUtil.c | 288 ---- AI/Interfaces/Java/src/main/native/JniUtil.h | 80 -- .../Java/src/main/native/JvmLocater.h | 53 - .../Java/src/main/native/JvmLocater_common.c | 188 --- .../Java/src/main/native/JvmLocater_common.h | 38 - .../Java/src/main/native/JvmLocater_linux.c | 266 ---- .../Java/src/main/native/JvmLocater_windows.c | 181 --- AI/Skirmish/NullJavaAI/CMakeLists.txt | 4 - AI/Skirmish/NullJavaAI/VERSION | 1 - .../NullJavaAI/bin/ant.dependent.properties | 36 - .../NullJavaAI/bin/ant.independent.properties | 29 - AI/Skirmish/NullJavaAI/bin/build.xml | 279 ---- AI/Skirmish/NullJavaAI/data/AIInfo.lua | 53 - AI/Skirmish/NullJavaAI/data/AIOptions.lua | 15 - AI/Skirmish/NullJavaAI/manifest.mf | 2 - .../NullJavaAI/src/nulljavaai/NullJavaAI.java | 187 --- AI/Skirmish/NullOOJavaAI/CMakeLists.txt | 4 - AI/Skirmish/NullOOJavaAI/VERSION | 1 - .../NullOOJavaAI/bin/ant.dependent.properties | 40 - .../bin/ant.independent.properties | 30 - AI/Skirmish/NullOOJavaAI/bin/build.xml | 342 ----- AI/Skirmish/NullOOJavaAI/data/AIInfo.lua | 53 - AI/Skirmish/NullOOJavaAI/data/AIOptions.lua | 15 - AI/Skirmish/NullOOJavaAI/manifest.mf | 2 - .../main/java/nulloojavaai/NullOOJavaAI.java | 320 ----- AI/Wrappers/JavaOO/CMakeLists.txt | 379 ----- AI/Wrappers/JavaOO/bin/ant.properties | 26 - AI/Wrappers/JavaOO/bin/build.xml | 211 --- AI/Wrappers/JavaOO/bin/wrappCallback.awk | 1241 ----------------- AI/Wrappers/JavaOO/bin/wrappEvents.awk | 627 --------- AI/Wrappers/JavaOO/jlib/vecmath-src.jar | Bin 183017 -> 0 bytes AI/Wrappers/JavaOO/jlib/vecmath.jar | Bin 310293 -> 0 bytes AI/Wrappers/JavaOO/pom.xml | 66 - .../java/com/springrts/ai/oo/AIEvent.java | 28 - .../java/com/springrts/ai/oo/AIException.java | 35 - .../java/com/springrts/ai/oo/AIFloat3.java | 118 -- .../springrts/ai/oo/CallbackAIException.java | 59 - .../com/springrts/ai/oo/EventAIException.java | 85 -- CMakeLists.txt | 7 +- rts/build/cmake/UtilJava.cmake | 101 -- 60 files changed, 1 insertion(+), 10390 deletions(-) delete mode 100644 AI/Interfaces/Java/CMakeLists.txt delete mode 100644 AI/Interfaces/Java/VERSION delete mode 100644 AI/Interfaces/Java/bin/ant.properties delete mode 100644 AI/Interfaces/Java/bin/build.xml delete mode 100755 AI/Interfaces/Java/bin/jni_wrappCallback.awk delete mode 100755 AI/Interfaces/Java/bin/jni_wrappCommands.awk delete mode 100755 AI/Interfaces/Java/bin/jni_wrappEvents.awk delete mode 100755 AI/Interfaces/Java/bin/native_createCallbackFPInitializations.awk delete mode 100755 AI/Interfaces/Java/bin/native_createCallbackFunctionImpls.awk delete mode 100755 AI/Interfaces/Java/bin/native_wrappCallback.awk delete mode 100755 AI/Interfaces/Java/bin/native_wrappCommands.awk delete mode 100644 AI/Interfaces/Java/data/InterfaceInfo.lua delete mode 100644 AI/Interfaces/Java/data/jvm.properties delete mode 100644 AI/Interfaces/Java/pom.xml delete mode 100644 AI/Interfaces/Java/src/main/java/com/springrts/ai/Util.java delete mode 100644 AI/Interfaces/Java/src/main/native/InterfaceDefines.h delete mode 100644 AI/Interfaces/Java/src/main/native/InterfaceExport.c delete mode 100644 AI/Interfaces/Java/src/main/native/InterfaceExport.h delete mode 100644 AI/Interfaces/Java/src/main/native/JavaBridge.c delete mode 100644 AI/Interfaces/Java/src/main/native/JavaBridge.h delete mode 100644 AI/Interfaces/Java/src/main/native/JniUtil.c delete mode 100644 AI/Interfaces/Java/src/main/native/JniUtil.h delete mode 100644 AI/Interfaces/Java/src/main/native/JvmLocater.h delete mode 100644 AI/Interfaces/Java/src/main/native/JvmLocater_common.c delete mode 100644 AI/Interfaces/Java/src/main/native/JvmLocater_common.h delete mode 100644 AI/Interfaces/Java/src/main/native/JvmLocater_linux.c delete mode 100644 AI/Interfaces/Java/src/main/native/JvmLocater_windows.c delete mode 100644 AI/Skirmish/NullJavaAI/CMakeLists.txt delete mode 100644 AI/Skirmish/NullJavaAI/VERSION delete mode 100644 AI/Skirmish/NullJavaAI/bin/ant.dependent.properties delete mode 100644 AI/Skirmish/NullJavaAI/bin/ant.independent.properties delete mode 100644 AI/Skirmish/NullJavaAI/bin/build.xml delete mode 100644 AI/Skirmish/NullJavaAI/data/AIInfo.lua delete mode 100644 AI/Skirmish/NullJavaAI/data/AIOptions.lua delete mode 100755 AI/Skirmish/NullJavaAI/manifest.mf delete mode 100644 AI/Skirmish/NullJavaAI/src/nulljavaai/NullJavaAI.java delete mode 100644 AI/Skirmish/NullOOJavaAI/CMakeLists.txt delete mode 100644 AI/Skirmish/NullOOJavaAI/VERSION delete mode 100644 AI/Skirmish/NullOOJavaAI/bin/ant.dependent.properties delete mode 100644 AI/Skirmish/NullOOJavaAI/bin/ant.independent.properties delete mode 100644 AI/Skirmish/NullOOJavaAI/bin/build.xml delete mode 100644 AI/Skirmish/NullOOJavaAI/data/AIInfo.lua delete mode 100644 AI/Skirmish/NullOOJavaAI/data/AIOptions.lua delete mode 100755 AI/Skirmish/NullOOJavaAI/manifest.mf delete mode 100644 AI/Skirmish/NullOOJavaAI/src/main/java/nulloojavaai/NullOOJavaAI.java delete mode 100644 AI/Wrappers/JavaOO/CMakeLists.txt delete mode 100644 AI/Wrappers/JavaOO/bin/ant.properties delete mode 100644 AI/Wrappers/JavaOO/bin/build.xml delete mode 100755 AI/Wrappers/JavaOO/bin/wrappCallback.awk delete mode 100755 AI/Wrappers/JavaOO/bin/wrappEvents.awk delete mode 100644 AI/Wrappers/JavaOO/jlib/vecmath-src.jar delete mode 100755 AI/Wrappers/JavaOO/jlib/vecmath.jar delete mode 100644 AI/Wrappers/JavaOO/pom.xml delete mode 100644 AI/Wrappers/JavaOO/src/main/java/com/springrts/ai/oo/AIEvent.java delete mode 100644 AI/Wrappers/JavaOO/src/main/java/com/springrts/ai/oo/AIException.java delete mode 100644 AI/Wrappers/JavaOO/src/main/java/com/springrts/ai/oo/AIFloat3.java delete mode 100644 AI/Wrappers/JavaOO/src/main/java/com/springrts/ai/oo/CallbackAIException.java delete mode 100644 AI/Wrappers/JavaOO/src/main/java/com/springrts/ai/oo/EventAIException.java delete mode 100644 rts/build/cmake/UtilJava.cmake diff --git a/AI/Interfaces/Java/CMakeLists.txt b/AI/Interfaces/Java/CMakeLists.txt deleted file mode 100644 index 9220868084e..00000000000 --- a/AI/Interfaces/Java/CMakeLists.txt +++ /dev/null @@ -1,661 +0,0 @@ -### Java AI Interface -# -# Global variables set in this file: -# * BUILD_Java_AIINTERFACE -# * Java_AIINTERFACE_VERS -# * Java_AIINTERFACE_TARGET -# -# Functions and macros defined in this file: -# * configure_java_skirmish_ai -# - -#enable_language(Java) - - -################################################################################ -### BEGIN: MACROS_AND_FUNCTIONS -# Define macros and functions to be used in this file and by Java Skirmish AIs - -# includes rts/build/cmake/UtilJava.cmake -include(UtilJava) - -# Java Skirmish AI configuration macro. -# This will be called from Java AIs at AI/Skirmish/*/CMakeLists.txt. -macro (configure_java_skirmish_ai wrapperNames) - # Assemble meta data - set(myDir "${CMAKE_CURRENT_SOURCE_DIR}") - get_last_path_part(dirName ${myDir}) - set(myName "${dirName}") - set(myJarFile "SkirmishAI") - set(myBinJarFile "${myJarFile}.jar") - set(mySrcJarFile "${myJarFile}-src.jar") - set(myJLibDir "${myDir}/data/jlib") - get_version_plus_dep_file(myVersion myVersionDepFile) - set(myTarget "${myName}") - set(myInstLibsDir "${SKIRMISH_AI_LIBS}/${myName}/${myVersion}") - set(myInstDataDir "${SKIRMISH_AI_DATA}/${myName}/${myVersion}") - # CMAKE_CURRENT_BINARY_DIR: .../spring-build-dir/AI/Skirmish/${myName} - set(myBuildDir "${CMAKE_CURRENT_BINARY_DIR}") - - # Check if the user wants to compile the AI - set(BUILD_THIS_SKIRMISHAI FALSE) - if (BUILD_Java_AIINTERFACE AND NOT myName MATCHES "${AI_EXCLUDE_REGEX}") - set(BUILD_THIS_SKIRMISHAI TRUE) - endif (BUILD_Java_AIINTERFACE AND NOT myName MATCHES "${AI_EXCLUDE_REGEX}") - if (NOT BUILD_THIS_SKIRMISHAI) - message("warning: ${myName} Skirmish AI will not be built!") - endif (NOT BUILD_THIS_SKIRMISHAI) - - # Compile and install - if (BUILD_THIS_SKIRMISHAI) - file(MAKE_DIRECTORY "${myBuildDir}/jlib") - - set(configString "default") - skirmish_ai_message(STATUS "Found Skirmish AI: ${myName} ${myVersion} (config: ${configString})") - - # Assemble project generated targets (and their libraries) we depend on - set(myDependTargets "${Java_AIINTERFACE_TARGET}") - set(myDependLibFiles "${Java_AIINTERFACE_JAR_BIN}") - foreach (wrapperName ${wrapperNames}) - set(myDependTargets ${myDependTargets} "${${wrapperName}_AIWRAPPER_TARGET}") - set(myDependLibFiles ${myDependLibFiles} "${${wrapperName}_AIWRAPPER_JAR_BIN}") - endforeach (wrapperName) - set_source_files_properties(${myDependLibFiles} PROPERTIES GENERATED TRUE) - - # find java source root - if (EXISTS "${myDir}/src/main/java") - # default Maven source dir - set(mySourceDir "${myDir}/src/main/java") - elseif (EXISTS "${myDir}/src") - # simple java source dir path - set(mySourceDir "${myDir}/src") - else (EXISTS "${myDir}/src/main/java") - message(SEND_ERROR "No sources dir found for Skirmish AI: ${myName}") - endif (EXISTS "${myDir}/src/main/java") - - # Create a list of all the AIs source files (for compiling) - file(GLOB_RECURSE mySources RELATIVE "${mySourceDir}" FOLLOW_SYMLINKS "${mySourceDir}/*.java") - # Create a list of all the AIs source files (for dependency tracking) - file(GLOB_RECURSE mySourcesDep FOLLOW_SYMLINKS "${mySourceDir}/*.java") - - set_source_files_properties("${myBuildDir}/${myBinJarFile}" PROPERTIES GENERATED TRUE) - set_source_files_properties("${myBuildDir}/${mySrcJarFile}" PROPERTIES GENERATED TRUE) - - - # If main Java package is "my.ai.pkg", this has to be set to "my". - get_first_sub_dir_name(firstSrcSubDir ${mySourceDir}) - set(myJavaPkgFirstPart "${firstSrcSubDir}") - - # Assemble additional meta data - set(myBinaryTarget "${myTarget}-BIN") - set(mySourceTarget "${myTarget}-SRC") - set(myJavaBuildDir "${myBuildDir}/classes") - - add_custom_target(${myBinaryTarget} - DEPENDS "${myBuildDir}/${myBinJarFile}") - add_dependencies(${myBinaryTarget} ${myDependTargets}) - - add_custom_target(${mySourceTarget} - DEPENDS "${myBuildDir}/${mySrcJarFile}") - - add_custom_target(${myTarget} ALL DEPENDS ${myVersionDepFile}) - add_dependencies(${myTarget} ${myBinaryTarget} ${mySourceTarget}) - - # Create our full Java class-path - create_classpath(myJavaLibs ${myJLibDir}) - concat_classpaths(myClassPath "${CLASSPATH_Java_AIINTERFACE}" "${myJavaLibs}") - foreach (wrapperName ${wrapperNames}) - concat_classpaths(myClassPath "${myClassPath}" "${${wrapperName}_AIWRAPPER_JAR_CLASSPATH}") - endforeach (wrapperName) - - # Locate the manifest file - find_manifest_file("${myDir}" myManifestFile) - if (myManifestFile) - set(myBinJarArgs "cmf" "${myManifestFile}") - else (myManifestFile) - set(myBinJarArgs "cf") - endif (myManifestFile) - - # Write list of source files to an arg-file - set(mySrcArgFile "${myBuildDir}/sourceFiles.txt") - if (EXISTS "${mySrcArgFile}") - file(REMOVE "${mySrcArgFile}") - endif (EXISTS "${mySrcArgFile}") - set(mySrcArgFile "${myBuildDir}/sourceFiles.txt") - foreach (srcFile ${mySources}) - file(APPEND "${mySrcArgFile}" "\"${srcFile}\"\n") - endforeach (srcFile) - - # Compile and pack the library - add_custom_command( - OUTPUT - "${myBuildDir}/${myBinJarFile}" - DEPENDS - ${myDependLibFiles} - ${mySourcesDep} - COMMAND "${CMAKE_COMMAND}" - "-E" "remove_directory" "${myJavaBuildDir}" - COMMAND "${CMAKE_COMMAND}" - "-E" "make_directory" "${myJavaBuildDir}" - COMMAND "${Java_JAVAC_EXECUTABLE}" - "${JAVA_COMPILE_FLAG_CONDITIONAL}" - "-Xlint:deprecation" - "-cp" "${myClassPath}" - "-d" "${myJavaBuildDir}" - "@${mySrcArgFile}" - COMMAND "${Java_JAR_EXECUTABLE}" - ${myBinJarArgs} "${myBuildDir}/${myBinJarFile}" - "-C" "${myJavaBuildDir}" "${myJavaPkgFirstPart}" - WORKING_DIRECTORY - "${mySourceDir}" - COMMENT - " ${myTarget}: Compiling sources and packing library ${myBinJarFile}" VERBATIM - ) - - # Pack the sources - add_custom_command( - OUTPUT - "${myBuildDir}/${mySrcJarFile}" - COMMAND "${Java_JAR_EXECUTABLE}" - "cf" "${myBuildDir}/${mySrcJarFile}" - "-C" "${mySourceDir}" "${myJavaPkgFirstPart}" - DEPENDS - ${mySourcesDeps} - WORKING_DIRECTORY - "${mySourceDir}" - COMMENT - " ${myTarget}: Creating sources archive ${mySrcJarFile}" VERBATIM - ) - - # Install the data files - install(DIRECTORY "${myDir}/data/" DESTINATION ${myInstDataDir}) - # Install the library - install(FILES "${myBuildDir}/${myBinJarFile}" DESTINATION ${myInstDataDir}) - # Install the sources archive (optional) - install(FILES "${myBuildDir}/${mySrcJarFile}" DESTINATION ${myInstDataDir}/jlib OPTIONAL) - - # Install files generated/downloaded during buildtime - install(DIRECTORY "${myBuildDir}/jlib/" DESTINATION ${myInstDataDir}/jlib) - if (EXISTS "${myBuildDir}/resources") - install(DIRECTORY "${myBuildDir}/resources/" DESTINATION ${myInstDataDir}/resources) - endif (EXISTS "${myBuildDir}/resources") - if (EXISTS "${myBuildDir}/config") - install(DIRECTORY "${myBuildDir}/config/" DESTINATION ${myInstDataDir}/config) - endif (EXISTS "${myBuildDir}/config") - if (EXISTS "${myBuildDir}/script") - install(DIRECTORY "${myBuildDir}/script/" DESTINATION ${myInstDataDir}/script) - endif (EXISTS "${myBuildDir}/script") - - # Install special script and config files - if (EXISTS "${myDir}/src/main/groovy") - install(DIRECTORY "${myDir}/src/main/groovy/" DESTINATION ${myInstDataDir}/script) - endif (EXISTS "${myDir}/src/main/groovy") - if (EXISTS "${myDir}/src/main/ruby") - install(DIRECTORY "${myDir}/src/main/ruby/" DESTINATION ${myInstDataDir}/script) - endif (EXISTS "${myDir}/src/main/ruby") - if (EXISTS "${myDir}/src/main/bsh") - install(DIRECTORY "${myDir}/src/main/bsh/" DESTINATION ${myInstDataDir}/script) - endif (EXISTS "${myDir}/src/main/bsh") - if (EXISTS "${myDir}/src/main/config") - install(DIRECTORY "${myDir}/src/main/config/" DESTINATION ${myInstDataDir}/config) - endif (EXISTS "${myDir}/src/main/config") - - # Install jars of wrappers - foreach (wrapperName ${wrapperNames}) - # Install the wrappers main jar - install(FILES "${${wrapperName}_AIWRAPPER_JAR_BIN}" DESTINATION ${myInstDataDir}/jlib) - set(wrapperJLibDir "${CMAKE_SOURCE_DIR}/AI/Wrappers/${wrapperName}/jlib") - # Install the wrappers java libs, if there are any - if (EXISTS ${wrapperJLibDir}) - install(DIRECTORY "${wrapperJLibDir}/" DESTINATION ${myInstDataDir}/jlib) - endif (EXISTS ${wrapperJLibDir}) - endforeach (wrapperName) - endif (BUILD_THIS_SKIRMISHAI) -endmacro (configure_java_skirmish_ai wrapperNames) - - -### END: MACROS_AND_FUNCTIONS -################################################################################ - - -set(myName "Java") -set(myDir "${CMAKE_CURRENT_SOURCE_DIR}") -set(mySourceDirRel "src/main") -set(myJavaSourceDirRel "src/main/java") -set(myNativeSourceDirRel "src/main/native") -set(myPkgFirstPart "com") -set(myPkg "${myPkgFirstPart}/springrts/ai") - - -# Check if the user wants to compile the interface -if ("${AI_TYPES}" STREQUAL "ALL" OR "${AI_TYPES}" STREQUAL "JAVA") - set(AI_TYPES_JAVA TRUE) -else ("${AI_TYPES}" STREQUAL "ALL" OR "${AI_TYPES}" STREQUAL "JAVA") - set(AI_TYPES_JAVA FALSE) -endif ("${AI_TYPES}" STREQUAL "ALL" OR "${AI_TYPES}" STREQUAL "JAVA") - -if (AI_TYPES_JAVA AND myName MATCHES "${AI_EXCLUDE_REGEX}") - set(AI_TYPES_JAVA FALSE) -endif (AI_TYPES_JAVA AND myName MATCHES "${AI_EXCLUDE_REGEX}") - - -# Look for dependencies, but only if the user wants to build the interface -if (AI_TYPES_JAVA) - if (NOT JAVA_FOUND) - find_package(Java REQUIRED COMPONENTS Development) - endif (NOT JAVA_FOUND) - if (MINGW) - set (JNI_FOUND TRUE) - else (MINGW) - # this hack is needed for FindJNI.cmake to use the JDK we want it to use, - # as otherwise it might not find one at all (eg. in the case of OpenJDK) - if ( NOT ENV{JAVA_HOME} AND JAVA_HOME ) - set(ENV{JAVA_HOME} "${JAVA_HOME}") - endif ( NOT ENV{JAVA_HOME} AND JAVA_HOME ) - find_package(JNI REQUIRED) - if (JAVA_INCLUDE_PATH) - set (JNI_FOUND TRUE) - include_directories(${JAVA_INCLUDE_PATH} ${JAVA_INCLUDE_PATH2} ${JNI_INCLUDE_DIRS}) - else (JAVA_INCLUDE_PATH) - set (JNI_FOUND FALSE) - message(WARNING "No Java includes found!") - endif (JAVA_INCLUDE_PATH) - endif (MINGW) -endif (AI_TYPES_JAVA) - - -# Check if dependencies of the interface are met -if (AI_TYPES_JAVA AND JNI_FOUND AND JAVA_FOUND AND EXISTS ${myDir} AND EXISTS ${myDir}/bin AND AWK_FOUND AND NOT myName MATCHES "${AI_EXCLUDE_REGEX}") - set_global(BUILD_${myName}_AIINTERFACE TRUE) -else (AI_TYPES_JAVA AND JNI_FOUND AND JAVA_FOUND AND EXISTS ${myDir} AND EXISTS ${myDir}/bin AND AWK_FOUND AND NOT myName MATCHES "${AI_EXCLUDE_REGEX}") - set_global(BUILD_${myName}_AIINTERFACE FALSE) - message("warning: Java AI Interface will not be built!") -endif (AI_TYPES_JAVA AND JNI_FOUND AND JAVA_FOUND AND EXISTS ${myDir} AND EXISTS ${myDir}/bin AND AWK_FOUND AND NOT myName MATCHES "${AI_EXCLUDE_REGEX}") - - -# Build -if (BUILD_${myName}_AIINTERFACE) - get_version_plus_dep_file(myVersion myVersionDepFile) - set(myTarget "${myName}-AIInterface") - set(myGenTarget "${myTarget}-generateSources") - set(myNativeTarget "${myTarget}-native") - set(myJavaTarget "${myTarget}-java") - set(myInstLibsDir ${AI_INTERFACES_LIBS}/${myName}/${myVersion}) - set(myInstDataDir ${AI_INTERFACES_DATA}/${myName}/${myVersion}) - set(myJavaSourceDirRel "${mySourceDirRel}/java") - set(myNativeSourceDirRel "${mySourceDirRel}/native") - make_absolute(mySourceDir "${myDir}" "${mySourceDirRel}") - make_absolute(myNativeSourceDir "${myDir}" "${myNativeSourceDirRel}") - make_absolute(myJavaSourceDir "${myDir}" "${myJavaSourceDirRel}") - - ai_interface_message(STATUS "Found AI Interface: ${myTarget} ${myVersion}") - - set_global(${myName}_AIINTERFACE_VERS ${myVersion}) - set_global(${myName}_AIINTERFACE_TARGET ${myTarget}) - set_global(${myName}_AIINTERFACE_TARGET_GENERATE_SOURCES ${myGenTarget}) - - - # Init some vars - # -------------- - set(myAwkScriptsDir "${myDir}/bin") - set(commonAwkScriptsDir "${CMAKE_SOURCE_DIR}/AI/Wrappers/CUtils/bin") - set(myBuildDir "${CMAKE_CURRENT_BINARY_DIR}") - set(springSourceDir "${PROJECT_SOURCE_DIR}") - set(springAIInterfaceSourceDir "${springSourceDir}/rts/ExternalAI/Interface") - set(myJavaBuildDir "${myBuildDir}/classes") - set(myJarFile "AIInterface") - set(myBinJarFile "${myJarFile}.jar") - set(mySrcJarFile "${myJarFile}-src.jar") - set(myGeneratedSourceDir "${myBuildDir}/src-generated/main") - set(myJavaGeneratedSourceDir "${myGeneratedSourceDir}/java") - set(myNativeGeneratedSourceDir "${myGeneratedSourceDir}/native") - set(myJLibDir "${myDir}/data/jlib") - create_classpath(myJavaLibs ${myJLibDir}) - set(myClassPath ".${PATH_DELIM_H}${myJavaLibs}${PATH_DELIM_H}${myJavaSourceDir}") - - # Used by Java Skirmish AIs - set_global(SOURCE_ROOT_${myName}_AIINTERFACE "${myDir}") - set_global(BUILD_ROOT_${myName}_AIINTERFACE "${myBuildDir}") - set_global(${myName}_AIINTERFACE_JAR_BIN "${myBuildDir}/${myBinJarFile}") - set_global(${myName}_AIINTERFACE_JAR_SRC "${myBuildDir}/${mySrcJarFile}") - set_global(${myName}_AIINTERFACE_POM "${myBuildDir}/pom-generated.xml") - set_global(CLASSPATH_${myName}_AIINTERFACE "${myJavaLibs}${PATH_DELIM_H}${myBuildDir}/${myBinJarFile}") - set_global(JAVA_SRC_DIR_${myName}_AIINTERFACE "${myJavaSourceDir}") - set_global(JAVA_GEN_SRC_DIR_${myName}_AIINTERFACE "${myJavaGeneratedSourceDir}") - - - # Generate sources - # ---------------- - - set(commonAwkScriptArgs - "-v" "SPRING_SOURCE_DIR=${springSourceDir}" - "-v" "INTERFACE_SOURCE_DIR=${myJavaSourceDir}" - "-v" "GENERATED_SOURCE_DIR=${myGeneratedSourceDir}" - "-v" "NATIVE_GENERATED_SOURCE_DIR=${myNativeGeneratedSourceDir}" - "-v" "JAVA_GENERATED_SOURCE_DIR=${myJavaGeneratedSourceDir}" - "-f" "${commonAwkScriptsDir}/common.awk" - "-f" "${commonAwkScriptsDir}/commonDoc.awk" - ) - - # A CMake Custom Target will always be built. - # from CMake docu: - # "add_custom_target is ALWAYS CONSIDERED OUT OF DATE" - - # Stub file for dependency tracking - set(myGeneratedSourceDirStubFile "${CMAKE_CURRENT_BINARY_DIR}/myGeneratedSourceDir.stub") - set_source_files_properties(${myGeneratedSourceDirStubFile} PROPERTIES GENERATED TRUE) - - - # source file lists (static and generated) - - set(myNativeSources - "${myNativeSourceDir}/InterfaceExport.c" - "${myNativeSourceDir}/JavaBridge.c" - "${myNativeSourceDir}/JniUtil.c" - "${myNativeSourceDir}/JvmLocater_common.c" - "${myNativeSourceDir}/JvmLocater_linux.c" - "${myNativeSourceDir}/JvmLocater_windows.c" - ) - set(myNativeGeneratedSources - "${myNativeGeneratedSourceDir}/CallbackFunctionPointerBridge.c" - "${myNativeGeneratedSourceDir}/CallbackJNIBridge.c" - "${myNativeGeneratedSourceDir}/EventsJNIBridge.c" - ) - set(myNativeGeneratedHeaders - "${myNativeGeneratedSourceDir}/CallbackFunctionPointerBridge.h" - "${myNativeGeneratedSourceDir}/CallbackJNIBridge.h" - "${myNativeGeneratedSourceDir}/EventsJNIBridge.h" - ) - - set(myJavaSources - "${myJavaSourceDir}/${myPkg}/Util.java" - ) - set(myJavaGeneratedSources - "${myJavaGeneratedSourceDir}/${myPkg}/AI.java" - "${myJavaGeneratedSourceDir}/${myPkg}/AbstractAI.java" - "${myJavaGeneratedSourceDir}/${myPkg}/AICallback.java" - "${myJavaGeneratedSourceDir}/${myPkg}/JniAICallback.java" - "${myJavaGeneratedSourceDir}/${myPkg}/Enumerations.java" - ) - - set(myGeneratedSources - ${myNativeGeneratedSources} - ${myNativeGeneratedHeaders} - ${myJavaGeneratedSources} - ) - set_source_files_properties(${myGeneratedSources} PROPERTIES GENERATED TRUE) - - - # remove all files in the generates sources dir, - # that are not generated sources (of this build) - file(GLOB_RECURSE allGeneratedSrcFiles "${myGeneratedSourceDir}/*") - foreach (genFile ${allGeneratedSrcFiles}) - list(FIND myGeneratedSources "${genFile}" isInList) - if (${isInList} EQUAL -1) - file(REMOVE "${genFile}") - endif () - endforeach (genFile) - - #cleanup generated dir if some input file changed - add_custom_command( - OUTPUT - ${myGeneratedSourceDirStubFile} - COMMAND "${CMAKE_COMMAND}" - "-E" "remove_directory" "${myGeneratedSourceDir}" - COMMAND "${CMAKE_COMMAND}" - "-E" "make_directory" "${myNativeGeneratedSourceDir}" - COMMAND "${CMAKE_COMMAND}" - "-E" "make_directory" "${myJavaGeneratedSourceDir}/${myPkg}" - COMMAND "${CMAKE_COMMAND}" - "-E" "touch" "${myGeneratedSourceDirStubFile}" - DEPENDS - "${commonAwkScriptsDir}/common.awk" - "${commonAwkScriptsDir}/commonDoc.awk" - "${myAwkScriptsDir}/native_wrappCallback.awk" - "${myAwkScriptsDir}/native_wrappCommands.awk" - "${myAwkScriptsDir}/jni_wrappCallback.awk" - "${myAwkScriptsDir}/jni_wrappCommands.awk" - "${myAwkScriptsDir}/jni_wrappEvents.awk" - "${springAIInterfaceSourceDir}/SSkirmishAICallback.h" - "${springAIInterfaceSourceDir}/AISCommands.h" - "${springAIInterfaceSourceDir}/AISEvents.h" - WORKING_DIRECTORY - "${myAwkScriptsDir}" - COMMENT - " ${myTarget}: Cleanup & create generated source directories" VERBATIM - ) - - # 1. & 2. Wrapp Callback (native->native) - add_custom_command( - OUTPUT - "${myNativeGeneratedSourceDir}/CallbackFunctionPointerBridge.h" - "${myNativeGeneratedSourceDir}/CallbackFunctionPointerBridge.c" - COMMAND "${AWK_BIN}" - ${commonAwkScriptArgs} - "-f" "${myAwkScriptsDir}/native_wrappCallback.awk" - "${springAIInterfaceSourceDir}/SSkirmishAICallback.h" - COMMAND "${AWK_BIN}" - ${commonAwkScriptArgs} - "-f" "${myAwkScriptsDir}/native_wrappCommands.awk" - "${springAIInterfaceSourceDir}/AISCommands.h" - DEPENDS - "${myGeneratedSourceDirStubFile}" - "${commonAwkScriptsDir}/common.awk" - "${commonAwkScriptsDir}/commonDoc.awk" - "${myAwkScriptsDir}/native_wrappCallback.awk" - "${myAwkScriptsDir}/native_wrappCommands.awk" - "${springAIInterfaceSourceDir}/SSkirmishAICallback.h" - "${springAIInterfaceSourceDir}/AISCommands.h" - WORKING_DIRECTORY - "${myAwkScriptsDir}" - COMMENT - " ${myTarget}: Generating native callback wrapper sources" VERBATIM - ) - - # 3. Wrapp AI Callback (native-JNI->Java) - add_custom_command( - OUTPUT - "${myNativeGeneratedSourceDir}/CallbackJNIBridge.h" - "${myNativeGeneratedSourceDir}/CallbackJNIBridge.c" - "${myJavaGeneratedSourceDir}/${myPkg}/AICallback.java" - "${myJavaGeneratedSourceDir}/${myPkg}/JniAICallback.java" - COMMAND "${AWK_BIN}" - ${commonAwkScriptArgs} - "-f" "${myAwkScriptsDir}/jni_wrappCallback.awk" - "${myNativeGeneratedSourceDir}/CallbackFunctionPointerBridge.h" - DEPENDS - "${myGeneratedSourceDirStubFile}" - "${commonAwkScriptsDir}/common.awk" - "${commonAwkScriptsDir}/commonDoc.awk" - "${myAwkScriptsDir}/jni_wrappCallback.awk" - "${myNativeGeneratedSourceDir}/CallbackFunctionPointerBridge.h" - WORKING_DIRECTORY - "${myAwkScriptsDir}" - COMMENT - " ${myTarget}: Generating JNI callback wrapper sources" VERBATIM - ) - - # 4. Wrapp AI Events (native-JNI->Java) - add_custom_command( - OUTPUT - "${myNativeGeneratedSourceDir}/EventsJNIBridge.h" - "${myNativeGeneratedSourceDir}/EventsJNIBridge.c" - "${myJavaGeneratedSourceDir}/${myPkg}/AI.java" - "${myJavaGeneratedSourceDir}/${myPkg}/AbstractAI.java" - COMMAND "${AWK_BIN}" - ${commonAwkScriptArgs} - "-f" "${myAwkScriptsDir}/jni_wrappEvents.awk" - "${springAIInterfaceSourceDir}/AISEvents.h" - DEPENDS - "${myGeneratedSourceDirStubFile}" - "${commonAwkScriptsDir}/common.awk" - "${commonAwkScriptsDir}/commonDoc.awk" - "${myAwkScriptsDir}/jni_wrappEvents.awk" - "${springAIInterfaceSourceDir}/AISEvents.h" - WORKING_DIRECTORY - "${myAwkScriptsDir}" - COMMENT - " ${myTarget}: Generating JNI events wrapper sources" VERBATIM - ) - # 5. Wrapp ENUMS - - add_custom_command( - OUTPUT - "${myJavaGeneratedSourceDir}/${myPkg}/Enumerations.java" - COMMAND "${AWK_BIN}" - ${commonAwkScriptArgs} - "-f" "${myAwkScriptsDir}/jni_wrappCommands.awk" - "${springAIInterfaceSourceDir}/AISCommands.h" - DEPENDS - "${myGeneratedSourceDirStubFile}" - "${commonAwkScriptsDir}/common.awk" - "${commonAwkScriptsDir}/commonDoc.awk" - "${myAwkScriptsDir}/jni_wrappCommands.awk" - "${springAIInterfaceSourceDir}/AISCommands.h" - WORKING_DIRECTORY - "${myAwkScriptsDir}" - COMMENT - " ${myTarget}: Generating enums wrapper sources" VERBATIM - ) - add_custom_target(${myGenTarget} DEPENDS ${myGeneratedSources}) - add_dependencies(generateSources ${myGenTarget}) - - # Locate the manifest file - find_manifest_file("${myDir}" myManifestFile) - if (myManifestFile) - set(myBinJarArgs "cmf" "${myManifestFile}") - else (myManifestFile) - set(myBinJarArgs "cf") - endif (myManifestFile) - - # Build the native part - # --------------------- - if (MINGW) - # It is important that this is used instead of the one - # from the installed JDK, as the jni_md.h is in here too, - # and this file contains OS (win32) specific information. - include_directories(BEFORE ${MINGWLIBS}/include/java) - endif (MINGW) - include_directories(BEFORE "${rts}/lib/streflop" "${myNativeSourceDir}" "${myNativeGeneratedSourceDir}") - add_library(${myNativeTarget} MODULE ${myNativeSources} ${myNativeGeneratedSources} ${ai_common_SRC} ${myVersionDepFile}) - add_dependencies(${myNativeTarget} generateVersionFiles) - target_link_libraries(${myNativeTarget} CUtils streflop) - set_target_properties(${myNativeTarget} PROPERTIES COMPILE_FLAGS "-DUSING_STREFLOP") - set_target_properties(${myNativeTarget} PROPERTIES OUTPUT_NAME "AIInterface") - fix_lib_name(${myNativeTarget}) - - - # Build the java part - # ------------------- - - # Write list of source files to an arg-file - set(mySrcArgFile "${myBuildDir}/sourceFiles.txt") - if (EXISTS "${mySrcArgFile}") - file(REMOVE "${mySrcArgFile}") - endif (EXISTS "${mySrcArgFile}") - foreach (srcFile ${myJavaSources} ${myJavaGeneratedSources}) - file(APPEND "${mySrcArgFile}" "\"${srcFile}\"\n") - endforeach (srcFile) - - add_custom_command( - OUTPUT - "${myBuildDir}/${myBinJarFile}" - COMMAND "${CMAKE_COMMAND}" - "-E" "remove_directory" "${myJavaBuildDir}" - COMMAND "${CMAKE_COMMAND}" - "-E" "make_directory" "${myJavaBuildDir}" - COMMAND "${Java_JAVAC_EXECUTABLE}" ARGS - "${JAVA_COMPILE_FLAG_CONDITIONAL}" - "-Xlint:deprecation" - "-cp" "${myClassPath}" - "-d" "${myJavaBuildDir}" - "@${mySrcArgFile}" - COMMAND "${Java_JAR_EXECUTABLE}" ARGS - ${myBinJarArgs} "${myBuildDir}/${myBinJarFile}" - "-C" "${myJavaBuildDir}" "${myPkgFirstPart}" - DEPENDS - ${myJavaSources} - # Using the native target reduces parallelism slightly - # (less then 1s per build => negliable), but prevetns multiple - # execution of the source generating custom-commands - # (which results in wrongly generated source files with "make -j ") - ${myJavaGeneratedSources} - ${myNativeTarget} - WORKING_DIRECTORY - "${myJavaGeneratedSourceDir}" - COMMENT - " ${myTarget}: Compiling sources and packing library ${myBinJarFile}" VERBATIM - ) - - add_custom_command( - OUTPUT - "${myBuildDir}/${mySrcJarFile}" - COMMAND "${Java_JAR_EXECUTABLE}" ARGS - "cf" "${myBuildDir}/${mySrcJarFile}" - "-C" "${myJavaSourceDir}" "${myPkgFirstPart}" - COMMAND "${Java_JAR_EXECUTABLE}" ARGS - "uf" "${myBuildDir}/${mySrcJarFile}" - "-C" "${myJavaGeneratedSourceDir}" "${myPkgFirstPart}" - COMMAND "${Java_JAR_EXECUTABLE}" ARGS - "uf" "${myBuildDir}/${mySrcJarFile}" - "-C" "${mySourceDir}" "native" - COMMAND "${Java_JAR_EXECUTABLE}" ARGS - "uf" "${myBuildDir}/${mySrcJarFile}" - "-C" "${myGeneratedSourceDir}" "native" - COMMAND "${Java_JAR_EXECUTABLE}" ARGS - "uf" "${myBuildDir}/${mySrcJarFile}" - "-C" "${myDir}" "VERSION" - DEPENDS - ${myJavaSources} - # Using the native target reduces parallelism slightly - # (less then 1s per build => negliable), but prevetns multiple - # execution of the source generating custom-commands - # (which results in wrongly generated source files with "make -j ") - ${myJavaGeneratedSources} - ${myNativeTarget} - WORKING_DIRECTORY - "${myBuildDir}" - COMMENT - " ${myTarget}: Creating sources archive ${mySrcJarFile}" VERBATIM - ) - - add_custom_target(${myJavaTarget} - DEPENDS - "${myBuildDir}/${myBinJarFile}" - "${myBuildDir}/${mySrcJarFile}" - ) - - add_custom_target(${myTarget} ALL) - add_dependencies(${myTarget} ${myNativeTarget}) - - # this sets the version in pom.xml - set(myMavenProperties "-Dmy.version=${myVersion}") - add_custom_command( - OUTPUT - "${myBuildDir}/pom-generated.xml" - COMMAND "${CMAKE_COMMAND}" - "-Dfile.in=${myDir}/pom.xml" - "-Dfile.out=${myBuildDir}/pom-generated.xml" - ${myMavenProperties} - "-P" "${CMAKE_MODULES_SPRING}/ConfigureFile.cmake" - DEPENDS - "${myDir}/pom.xml" - WORKING_DIRECTORY - "${myDir}" - COMMENT - " ${myTarget}: Configure pom.xml" VERBATIM - ) - set_source_files_properties("${myBuildDir}/pom-generated.xml" PROPERTIES GENERATED TRUE) - - add_dependencies(${myTarget} ${myJavaTarget}) - - # Install the native library - install(TARGETS ${myNativeTarget} DESTINATION ${myInstLibsDir}) - # Install the data files - install(DIRECTORY "${myDir}/data/" DESTINATION ${myInstLibsDir} FILES_MATCHING PATTERN REGEX "InterfaceInfo\\.lua$") - install(DIRECTORY "${myDir}/data/" DESTINATION ${myInstDataDir} FILES_MATCHING PATTERN REGEX "InterfaceInfo\\.lua$" EXCLUDE PATTERN "*") - # Install the library - install(FILES "${myBuildDir}/${myBinJarFile}" DESTINATION ${myInstDataDir}) - # Install the sources archive - install(FILES "${myBuildDir}/${mySrcJarFile}" DESTINATION ${myInstDataDir}/jlib) -endif (BUILD_${myName}_AIINTERFACE) diff --git a/AI/Interfaces/Java/VERSION b/AI/Interfaces/Java/VERSION deleted file mode 100644 index ceab6e11ece..00000000000 --- a/AI/Interfaces/Java/VERSION +++ /dev/null @@ -1 +0,0 @@ -0.1 \ No newline at end of file diff --git a/AI/Interfaces/Java/bin/ant.properties b/AI/Interfaces/Java/bin/ant.properties deleted file mode 100644 index b9c0a35d3ec..00000000000 --- a/AI/Interfaces/Java/bin/ant.properties +++ /dev/null @@ -1,21 +0,0 @@ -# Paths are relative to the project home (which is ../ from this file). -# All values are optional. - -;spring.home=../../.. - -# This is used only in the next property -;build.home=${spring.home}/build -# Where jar files will be built -;build.dir=${build.home}/AI/Interfaces/${interface.name} - -# Where generated sources shall be created in -;src.generated=${build.dir}/src-generated/main -;src.generated.native=${src.generated}/native -;src.generated.java=${src.generated}/java - -# This is used only in the next property -;dist.home=${spring.home}/dist -# Where jar files will be installed to -;dist.dir=${dist.home}/AI/Interfaces/${interface.name}/${interface.version} -# Where HTML JavaDoc files will be generated -;doc.dir=${dist.home}/AI/Interfaces/${interface.name}/${interface.version}/doc/jdoc diff --git a/AI/Interfaces/Java/bin/build.xml b/AI/Interfaces/Java/bin/build.xml deleted file mode 100644 index 86e0033068a..00000000000 --- a/AI/Interfaces/Java/bin/build.xml +++ /dev/null @@ -1,258 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/AI/Interfaces/Java/bin/jni_wrappCallback.awk b/AI/Interfaces/Java/bin/jni_wrappCallback.awk deleted file mode 100755 index 833bdb90ec3..00000000000 --- a/AI/Interfaces/Java/bin/jni_wrappCallback.awk +++ /dev/null @@ -1,573 +0,0 @@ -#!/usr/bin/awk -f -# -# This awk script creates a Java class with native/JNI functions, -# plus their respective native counterparts, -# to call to C functions in: -# NATIVE_GENERATED_SOURCE_DIR/CallbackFunctionPointerBridge.h -# -# This script uses functions from the following files: -# * common.awk -# * commonDoc.awk -# Variables that can be set on the command-line (with -v): -# * GENERATED_SOURCE_DIR: the generated sources root dir -# -# usage: -# awk -f thisScript.awk -f common.awk -f commonDoc.awk -# awk -f thisScript.awk -f common.awk -f commonDoc.awk \ -# -v 'GENERATED_SOURCE_DIR=/tmp/build/AI/Interfaces/Java/src-generated/main' -# - -BEGIN { - # initialize things - - # define the field splitter(-regex) - FS = "(,)|(\\()|(\\);)"; - - # Used by other scripts - JAVA_MODE = 1; - - # These vars can be assigned externally, see file header. - # Set the default values if they were not supplied on the command line. - if (!GENERATED_SOURCE_DIR) { - GENERATED_SOURCE_DIR = "../src-generated/main"; - } - if (!JAVA_GENERATED_SOURCE_DIR) { - JAVA_GENERATED_SOURCE_DIR = GENERATED_SOURCE_DIR "/java"; - } - if (!NATIVE_GENERATED_SOURCE_DIR) { - NATIVE_GENERATED_SOURCE_DIR = GENERATED_SOURCE_DIR "/native"; - } - - nativeBridge = "CallbackFunctionPointerBridge"; - bridgePrefix = "bridged__"; - - jniBridge = "CallbackJNIBridge"; - - myPkgA = "com.springrts.ai"; - myPkgD = convertJavaNameFormAToD(myPkgA); - myPkgC = convertJavaNameFormAToC(myPkgA); - myInterface = "AICallback"; - myClass = "JniAICallback"; - - fi = 0; -} - -function doWrapp(funcIndex_dw) { - - paramListJava_dw = funcParamList[funcIndex_dw]; - doWrapp_dw = 1; - - fullName_dw = funcFullName[funcIndex_dw]; - if (doWrapp_dw) { - metaInf_dw = funcMetaInf[funcIndex_dw]; - - if (match(metaInf_dw, /ARRAY:/)) { - #doWrapp_dw = 0; - } - if (match(metaInf_dw, /MAP:/)) { - #doWrapp_dw = 0; - } - if (match(fullName_dw, "^" bridgePrefix "File_")) { - doWrapp_dw = 0; - } - } else { - print("Java-AIInterface: NOTE: JNI level: Callback: intentionally not wrapped: " fullName_dw); - } - - return doWrapp_dw; -} - -function createNativeFileName(fileName_fn, isHeader_fn) { - - absFileName_fn = NATIVE_GENERATED_SOURCE_DIR "/" fileName_fn; - if (isHeader_fn) { - absFileName_fn = absFileName_fn ".h"; - } else { - absFileName_fn = absFileName_fn ".c"; - } - - return absFileName_fn; -} - -function printNativeJNI() { - - outFile_nh = createNativeFileName(jniBridge, 1); - outFile_nc = createNativeFileName(jniBridge, 0); - - printCommentsHeader(outFile_nh); - printCommentsHeader(outFile_nc); - - print("") >> outFile_nh; - print("#ifndef __CALLBACK_JNI_BRIDGE_H") >> outFile_nh; - print("#define __CALLBACK_JNI_BRIDGE_H") >> outFile_nh; - print("") >> outFile_nh; - print("#include ") >> outFile_nh; - print("") >> outFile_nh; - print("#ifdef __cplusplus") >> outFile_nh; - print("extern \"C\" {") >> outFile_nh; - print("#endif") >> outFile_nh; - print("") >> outFile_nh; - - print("") >> outFile_nc; - print("#include \"" jniBridge ".h\"") >> outFile_nc; - print("") >> outFile_nc; - print("#include \"" nativeBridge ".h\"") >> outFile_nc; - print("") >> outFile_nc; - print("") >> outFile_nc; - - # print the wrapping functions - for (i=0; i < fi; i++) { - fullName = funcFullName[i]; - retType = funcRetTypeC[i]; - paramList = funcParamListC[i]; - paramListNoTypes = removeParamTypes(paramList); - metaInf = funcMetaInf[i]; - - if (doWrapp(i)) { - isRetString = part_isRetString(fullName, metaInf); - if (isRetString) { - retString = part_getRetString(metaInf); - split(retString, retStringNames, ":"); # must return 2 elements - } - - javaName = fullName; - sub("^" bridgePrefix, "", javaName); - gsub(/_/, "_1", javaName); - jni_funcName = "Java_" myPkgC "_" myClass "_" javaName; - jni_retType = convertCToJNIType(retType); - jni_paramList = ""; - size_params = split(paramList, params, ","); - for (p=1; p <= size_params; p++) { - pType_c = params[p]; - sub(/ [^ ]+$/, "", pType_c); - pType_c = trim(pType_c); - - pName = params[p]; - sub(/^.* /, "", pName); - - pType_jni = convertCToJNIType(pType_c, pName); - - # these are later used for conversion - c_paramTypes[p] = pType_c; - c_paramNames[p] = pName; - jni_paramTypes[p] = pType_jni; - jni_paramNames[p] = pName; - - if (isRetString && (pName == retStringNames[1] || pName == retStringNames[2])) { - if (pName == retStringNames[1]) - jni_retType = convertCToJNIType(pType_c); - continue; - } - jni_paramList = jni_paramList ", " pType_jni " " pName; - } - jni_paramListNoTypes = removeParamTypes(jni_paramList); - isVoidRet = match(jni_retType, /^void$/); - - # print function declaration to *.h - #printFunctionComment_Common(outFile_nh, funcDocComment, i, ""); - print("JNIEXPORT " jni_retType " JNICALL " jni_funcName "(JNIEnv* __env, jobject __obj" jni_paramList ");") >> outFile_nh; - print("") >> outFile_nh; - - # print function definition to *.c - print("JNIEXPORT " jni_retType " JNICALL " jni_funcName "(JNIEnv* __env, jobject __obj" jni_paramList ") {") >> outFile_nc; - print("") >> outFile_nc; - - if (!isVoidRet) { - print("\t" jni_retType " _ret;") >> outFile_nc; - print("") >> outFile_nc; - } - - # Return value conversion - pre call - retType_isString = ((match(retType, /^(const )?char*/) || isRetString) && (jni_retType == "jstring")); - retTypeConv = 0; - if (retType_isString) { - if (isRetString) - print("\t" "char " retStringNames[1] "[2048] = {'\\0'};") >> outFile_nc; - print("\t" retType " _retNative;") >> outFile_nc; - retTypeConv = 1; - } - - hasRetParam = 0; - # Params conversion - pre call - for (p=1; p <= size_params; p++) { - pType_jni = jni_paramTypes[p]; - - if (pType_jni == "jstring") { - # jstring - if (!isRetString || c_paramNames[p] != retStringNames[1]) { - c_paramNames[p] = c_paramNames[p] "_native"; - sub(" " jni_paramNames[p], " " c_paramNames[p], paramListNoTypes); - print("\t" c_paramTypes[p] " " c_paramNames[p] " = (" c_paramTypes[p] ") (*__env)->GetStringUTFChars(__env, " jni_paramNames[p] ", NULL);") >> outFile_nc; - } - } else if (pType_jni == "jint") { - if (isRetString && c_paramNames[p] == retStringNames[2]) - sub(" " jni_paramNames[p], " sizeof(" retStringNames[1] ")", paramListNoTypes); - } else if (match(pType_jni, /^j.+Array$/)) { - # primitive array - c_paramNames[p] = c_paramNames[p] "_native"; - sub(" " jni_paramNames[p], " " c_paramNames[p], paramListNoTypes); - - capArrType = pType_jni; - sub(/^j/, "", capArrType); - sub(/Array$/, "", capArrType); - capArrType = capitalize(capArrType); - - _isPrimitive = (capArrType != "Object"); - _isString = !_isPrimitive && match(c_paramTypes[p], /(const )?char\*\*/); - - print("\t" c_paramTypes[p] " " c_paramNames[p] " = NULL;") >> outFile_nc; - print("\t" "if (" jni_paramNames[p] " != NULL) {") >> outFile_nc; - if (_isPrimitive) { - print("\t\t" c_paramNames[p] " = (" c_paramTypes[p] ") (*__env)->Get" capArrType "ArrayElements(__env, " jni_paramNames[p] ", NULL);") >> outFile_nc; - } else if (_isString) { - print("\t\t" "const int " c_paramNames[p] "_size = (int) (*__env)->GetArrayLength(__env, " jni_paramNames[p] ");") >> outFile_nc; - print("\t\t" c_paramNames[p] " = (" c_paramTypes[p] ") malloc(sizeof(char*) * " c_paramNames[p] "_size);") >> outFile_nc; - } else { - print("ERROR: do not know how to convert parameter type: " pType_jni); - exit(1); - } - print("\t" "}") >> outFile_nc; - } else if (pType_jni == "jobject") { - # StringBuffer - hasRetParam = 1; - cPaNa = c_paramNames[p]; - c_paramNames[p] = cPaNa "_native"; - sub(" " jni_paramNames[p], " " c_paramNames[p], paramListNoTypes); - print("\t" "char " c_paramNames[p] "[MAX_RESPONSE_SIZE];") >> outFile_nc; - retParamConversion = "\t" "jclass clazz = (*__env)->GetObjectClass(__env, " cPaNa ");" "\n"; - retParamConversion = retParamConversion "\t" "jmethodID mid = (*__env)->GetMethodID(__env, clazz, \"append\", \"(Ljava/lang/String;)Ljava/lang/StringBuffer;\");" "\n" - retParamConversion = retParamConversion "\t" "jstring " cPaNa "_jStr = (*__env)->NewStringUTF(__env, " c_paramNames[p] ");" "\n"; - retParamConversion = retParamConversion "\t" "(*__env)->CallObjectMethod(__env, " cPaNa ", mid, " cPaNa "_jStr);" - } - } - - condRet = ""; - if (!isVoidRet) { - if (retTypeConv) { - condRet = "_retNative = "; - } else { - condRet = "_ret = (" jni_retType ") "; - } - } - print("\t" condRet fullName "(" paramListNoTypes ");") >> outFile_nc; - if (hasRetParam) { - print(retParamConversion) >> outFile_nc; - } - - # Params conversion - post call - for (p=1; p <= size_params; p++) { - pType_jni = jni_paramTypes[p]; - - if (pType_jni == "jstring") { - # jstring - if (!isRetString || c_paramNames[p] != retStringNames[1]) - print("\t" "(*__env)->ReleaseStringUTFChars(__env, " jni_paramNames[p] ", " c_paramNames[p] ");") >> outFile_nc; - } else if (match(pType_jni, /^j.+Array$/)) { - # primitive array - capArrType = pType_jni; - sub(/^j/, "", capArrType); - sub(/Array$/, "", capArrType); - capArrType = capitalize(capArrType); - - _isPrimitive = (capArrType != "Object"); - _isString = !_isPrimitive && match(c_paramTypes[p], /(const )?char\*\*/); - - print("\t" "if (" jni_paramNames[p] " != NULL) {") >> outFile_nc; - if (_isPrimitive) { - _elementJNativeType = jni_paramTypes[p]; - sub(/Array$/, "", _elementJNativeType); # jfloatArray -> jfloat - print("\t\t" "(*__env)->Release" capArrType "ArrayElements(__env, " jni_paramNames[p] ", (" _elementJNativeType "*) " c_paramNames[p] ", 0 /* copy back changes and release */);") >> outFile_nc; - } else if (_isString) { - print("\t\t" "const int " c_paramNames[p] "_size = (int) (*__env)->GetArrayLength(__env, " jni_paramNames[p] ");") >> outFile_nc; - print("\t\t" "int " c_paramNames[p] "_i;") >> outFile_nc; - print("\t\t" "jstring " c_paramNames[p] "_jStr;") >> outFile_nc; - print("\t\t" "for (" c_paramNames[p] "_i=0; " c_paramNames[p] "_i < " c_paramNames[p] "_size; ++" c_paramNames[p] "_i) {") >> outFile_nc; - print("\t\t\t" c_paramNames[p] "_jStr = (jstring) (*__env)->NewStringUTF(__env, " c_paramNames[p] "[" c_paramNames[p] "_i]);") >> outFile_nc; - print("\t\t\t" "(*__env)->SetObjectArrayElement(__env, " jni_paramNames[p] ", " c_paramNames[p] "_i, " c_paramNames[p] "_jStr);") >> outFile_nc; - print("\t\t\t" "(*__env)->DeleteLocalRef(__env, " c_paramNames[p] "_jStr);") >> outFile_nc; - print("\t\t" "}") >> outFile_nc; - print("\t\t" "free(" c_paramNames[p] ");") >> outFile_nc; - } - print("\t" "}") >> outFile_nc; - } else if (pType_jni == "jobject" ) { - # StringBuffer - print("\t" "(*__env)->DeleteLocalRef(__env, " cPaNa "_jStr);") >> outFile_nc; - } - } - - # Return value conversion - post call - if (retType_isString) { - print("\t" "_ret = (*__env)->NewStringUTF(__env, " (isRetString ? retStringNames[1] : "_retNative") ");") >> outFile_nc; - } - - if (!isVoidRet) { - print("") >> outFile_nc; - print("\t" "return _ret;") >> outFile_nc; - } - print("" "}") >> outFile_nc; - print("") >> outFile_nc; - } else { - print("Note: The following function is intentionally not wrapped: " fullName); - } - } - - - print("#ifdef __cplusplus") >> outFile_nh; - print("} // extern \"C\"") >> outFile_nh; - print("#endif") >> outFile_nh; - print("") >> outFile_nh; - print("#endif // __CALLBACK_JNI_BRIDGE_H") >> outFile_nh; - print("") >> outFile_nh; - - close(outFile_nh); - close(outFile_nc); -} - - -function printHeader(outFile_h, javaPkg_h, javaClassName_h) { - - printCommentsHeader(outFile_h); - print("") >> outFile_h; - print("package " javaPkg_h ";") >> outFile_h; - print("") >> outFile_h; - print("") >> outFile_h; - print("/**") >> outFile_h; - print(" * Lets Java Skirmish AIs call back to the Spring engine.") >> outFile_h; - print(" * We are using JNI for best speed.") >> outFile_h; - print(" *") >> outFile_h; - print(" * @author AWK wrapper script") >> outFile_h; - print(" * @version GENERATED") >> outFile_h; - print(" */") >> outFile_h; - if (javaClassName_h == myClass) { - print("public class " javaClassName_h " implements " myInterface " {") >> outFile_h; - } else { - print("public interface " javaClassName_h " {") >> outFile_h; - } - print("") >> outFile_h; -} - -function createJavaFileName(clsName_f) { - return JAVA_GENERATED_SOURCE_DIR "/" myPkgD "/" clsName_f ".java"; -} - -function printJavaClsAndInt() { - - outFile_i = createJavaFileName(myInterface); - outFile_c = createJavaFileName(myClass); - - printHeader(outFile_i, myPkgA, myInterface); - printHeader(outFile_c, myPkgA, myClass); - - # print the static registrator - print("\tstatic {") >> outFile_c; - print("\t\tSystem.loadLibrary(\"AIInterface\");") >> outFile_c; - print("\t}") >> outFile_c; - print("") >> outFile_c; - - # print skirmishAIId getter in interface - print("\t" "public int SkirmishAI_getSkirmishAIId();") >> outFile_i; - print("") >> outFile_i; - - # print skirmishAIId member, constructor and getter - print("\t" "private int skirmishAIId;") >> outFile_c; - print("") >> outFile_c; - print("\t" "public " myClass "(int skirmishAIId) {") >> outFile_c; - print("\t\t" "this.skirmishAIId = skirmishAIId;") >> outFile_c; - print("\t}") >> outFile_c; - print("") >> outFile_c; - print("\t" "@Override") >> outFile_c; - print("\t" "public int SkirmishAI_getSkirmishAIId() {") >> outFile_c; - print("\t\t" "return this.skirmishAIId;") >> outFile_c; - print("\t}") >> outFile_c; - print("") >> outFile_c; - - # print the callback methods - for (i=0; i < fi; i++) { - if (doWrapp(i)) { - fullName = funcFullName[i]; - retType = funcRetTypeJ[i]; - paramList = funcParamListJ[i]; - metaInf = funcMetaInf[i]; - - isRetString = part_isRetString(fullName, metaInf); - if (isRetString) { - retString = part_getRetString(metaInf); - split(retString, retStringNames, ":"); # must return 2 elements - size_params = split(paramList, params, ","); - for (p=1; p <= size_params; p++) { - pName = params[p]; - sub(/^.* /, "", pName); - if (pName == retStringNames[1]) { - ps = 1; - } else if (pName == retStringNames[2]) { - ps = 2; - } else continue; - pType_c = params[p]; - sub(/ [^ ]+$/, "", pType_c); - pType_c = trim(pType_c); - retStringTypes[ps] = convertJNIToJavaType(convertCToJNIType(pType_c, pName)); - } - retType = retStringTypes[1]; - sub(", " retStringTypes[1] " " retStringNames[1] ", " retStringTypes[2] " " retStringNames[2], "", paramList); - } - - paramListNoSID = paramList; - sub(/int _skirmishAIId(, )?/, "", paramListNoSID); - paramListNoSIDNoTypes = removeParamTypes(paramListNoSID); - if (paramListNoSIDNoTypes != "") { - paramListNoSIDNoTypes = ", " paramListNoSIDNoTypes; - } - condRet = ""; - if (retType != "void") { - condRet = "return "; - } - - sub("^" bridgePrefix, "", fullName); - - metaInfCommand = ""; - if (metaInf != "") { - metaInfCommand = " // " metaInf; - } - - # print the interface function - printFunctionComment_Common(outFile_i, funcDocComment, i, "\t"); - print("\t" "public " retType " " fullName "(" paramListNoSID ");" metaInfCommand) >> outFile_i; - print("") >> outFile_i; - - # print the interface implementing function - commentText = getFunctionComment_Common(funcDocComment, i); - if (match(commentText, /@deprecated/)) { - # this prevents javac from outputting a warning - print("\t" "/** @deprecated */") >> outFile_c; - } - print("\t" "@Override") >> outFile_c; - print("\t" "public " retType " " fullName "(" paramListNoSID ") {") >> outFile_c; - print("\t\t" condRet "this." fullName "(this.skirmishAIId" paramListNoSIDNoTypes ");") >> outFile_c; - print("\t" "}") >> outFile_c; - - # print the private native function - print("\t" "private native " retType " " fullName "(" paramList ");") >> outFile_c; - print("") >> outFile_c; - } - } - - print("}") >> outFile_i; - print("") >> outFile_i; - close(outFile_i); - - print("}") >> outFile_c; - print("") >> outFile_c; - close(outFile_c); -} - - -function wrappFunction(funcDef, commentEol) { - - doParse = 1; - - if (doParse) { - size_funcParts = split(funcDef, funcParts, "(,)|(\\()|(\\))"); - # because the empty part after ");" would count as part as well - size_funcParts--; - retType_c = trim(funcParts[2]); - retType_j = convertJNIToJavaType(convertCToJNIType(retType_c)); - fullName = trim(funcParts[3]); - - # function parameters - paramList_c = ""; - paramList_j = ""; - paramList = ""; - - for (i=4; i<=size_funcParts && !match(funcParts[i], /.*\/\/.*/); i++) { - type_c = extractParamType(funcParts[i]); - type_c = cleanupCType(type_c); - name = extractParamName(funcParts[i]); - type_j = convertJNIToJavaType(convertCToJNIType(type_c, name)); - if (i == 4) { - cond_comma = ""; - } else { - cond_comma = ", "; - } - paramList_c = paramList_c cond_comma type_c " " name; - paramList_j = paramList_j cond_comma type_j " " name; - } - - funcFullName[fi] = fullName; - funcRetTypeC[fi] = retType_c; - funcRetTypeJ[fi] = retType_j; - funcParamListC[fi] = paramList_c; - funcParamListJ[fi] = paramList_j; - funcMetaInf[fi] = trim(commentEol); - storeDocLines(funcDocComment, fi); - fi++; - } else { - print("warning: function intentionally NOT wrapped: " funcDef); - } -} - - - -# This function has to return true (1) if a doc comment (eg: /** foo bar */) -# can be deleted. -# If there is no special condition you want to apply, -# it should always return true (1), -# cause there are additional mechanism to prevent accidental deleting. -# see: commonDoc.awk -function canDeleteDocumentation() { - return isMultiLineFunc != 1; -} - - - -# save function pointer info into arrays -# ... 2nd, 3rd, ... line of a function pointer definition -{ - if (isMultiLineFunc) { # function is defined on one single line - funcIntermLine = $0; - # separate possible comment at end of line: // fu bar - commentEol = funcIntermLine; - if (sub(/.*\/\//, "", commentEol)) { - commentEolTot = commentEolTot commentEol; - } - # remove possible comment at end of line: // fu bar - sub(/[ \t]*\/\/.*/, "", funcIntermLine); - funcIntermLine = trim(funcIntermLine); - funcSoFar = funcSoFar " " funcIntermLine; - if (match(funcSoFar, /;$/)) { - # function ends in this line - wrappFunction(funcSoFar, commentEolTot); - isMultiLineFunc = 0; - } - } -} -# 1st line of a function pointer definition -/^EXPORT\(/ { - - funcStartLine = $0; - # separate possible comment at end of line: // foo bar - commentEolTot = ""; - commentEol = funcStartLine; - if (sub(/.*\/\//, "", commentEol)) { - commentEolTot = commentEolTot commentEol; - } - # remove possible comment at end of line: // fu bar - sub(/\/\/.*$/, "", funcStartLine); - funcStartLine = trim(funcStartLine); - if (match(funcStartLine, /;$/)) { - # function ends in this line - wrappFunction(funcStartLine, commentEolTot); - } else { - funcSoFar = funcStartLine; - isMultiLineFunc = 1; - } -} - - - - -END { - # finalize things - - printNativeJNI(); - printJavaClsAndInt(); -} diff --git a/AI/Interfaces/Java/bin/jni_wrappCommands.awk b/AI/Interfaces/Java/bin/jni_wrappCommands.awk deleted file mode 100755 index abd4c0e62e1..00000000000 --- a/AI/Interfaces/Java/bin/jni_wrappCommands.awk +++ /dev/null @@ -1,192 +0,0 @@ -#!/usr/bin/awk -f -# -# This awk script creates a Java class containing important enumerations. -# These enumerations are taken from: rts/ExternalAI/Interface/AISCommands.h -# It currently only takes CommandTopic and UnitCommandOptions enumerations -# -# This script uses functions from the following files: -# * common.awk -# * commonDoc.awk -# Variables that can be set on the command-line (with -v): -# * GENERATED_SOURCE_DIR: the generated sources root dir -# -# usage: -# awk -f thisScript.awk -f common.awk -f commonDoc.awk -# awk -f thisScript.awk -f common.awk -f commonDoc.awk \ -# -v 'GENERATED_SOURCE_DIR=/tmp/build/AI/Interfaces/Java/src-generated/main' -# - -BEGIN { - # initialize things - - # define the field splitter(-regex) - FS = "[ \t]+"; - - # Used by other scripts - JAVA_MODE = 1; - - # These vars can be assigned externally, see file header. - # Set the default values if they were not supplied on the command line. - if (!GENERATED_SOURCE_DIR) { - GENERATED_SOURCE_DIR = "../src-generated/main"; - } - if (!JAVA_GENERATED_SOURCE_DIR) { - JAVA_GENERATED_SOURCE_DIR = GENERATED_SOURCE_DIR "/java"; - } - if (!NATIVE_GENERATED_SOURCE_DIR) { - NATIVE_GENERATED_SOURCE_DIR = GENERATED_SOURCE_DIR "/native"; - } - - myPkgA = "com.springrts.ai"; - myPkgD = convertJavaNameFormAToD(myPkgA); - myClass = "Enumerations"; - - #create empty arrays, holding names and values of the two enumerations - cmdsTopicNamesLength = 0; - split("", cmdsTopicNames); - cmdsTopicValuesLength = 0; - split("", cmdsTopicValues); - unitCmdsTopicNamesLength = 0; - split("", unitCmdsTopicNames); - unitCmdsTopicValuesLength = 0; - split("", unitCmdsTopicValues); -} - - -function createJavaFileName(fileName_fn) { - return JAVA_GENERATED_SOURCE_DIR "/" myPkgD "/" fileName_fn ".java"; -} - -function printGeneralJavaHeader(outFile_h, javaPkg_h, javaClassName_h) { - - printCommentsHeader(outFile_h); - print("") >> outFile_h; - print("package " javaPkg_h ";") >> outFile_h; - print("") >> outFile_h; - print("") >> outFile_h; - print("/**") >> outFile_h; - print(" * These are the Java exposed enumerations.") >> outFile_h; - print(" * We are not calling the engine, this is a pure Java class.") >> outFile_h; - print(" *") >> outFile_h; - print(" * @author AWK wrapper script") >> outFile_h; - print(" * @version GENERATED") >> outFile_h; - print(" */") >> outFile_h; - print("public abstract class " javaClassName_h " {") >> outFile_h; - print("") >> outFile_h; -} - -function printJavaHeader() { - - outFile_i = createJavaFileName(myClass); - - printGeneralJavaHeader(outFile_i, myPkgA, myClass); -} - -function printJavaEnums(enums) { - printJavaEnum("CommandTopic", cmdsTopicNames, cmdsTopicNamesLength, cmdsTopicValues); - print("") >> outFile_i; - printJavaEnum("UnitCommandOptions", unitCmdsTopicNames, unitCmdsTopicNamesLength, unitCmdsTopicValues); -} - -function printJavaEnum(enumName, names, namesLength, values) { - outFile_i = createJavaFileName(myClass); - printEnumHeader(enumName); - - # Prints the enum members and values - first = 0; - for (i=0; i> outFile_i; - } else { - printf(",\n\t\t") >> outFile_i; - } - printJavaEnumItem(names[i], values[i]); - } - - if (first != 0) { - print(";\n") >> outFile_i; - } - printEnumEnd(enumName); -} - -function printEnumHeader(name) { - outFile_i = createJavaFileName(myClass); - - # Prints the enum start - print("\tpublic enum " name " {") >> outFile_i; -} - -function printEnumEnd(name) { - outFile_i = createJavaFileName(myClass); - - # Prints the enum end - print("\t\tprivate final int id;") >> outFile_i; - print("\t\t" name "(int id) { this.id = id; }") >> outFile_i; - print("\t\tpublic int getValue() { return id; }") >> outFile_i; - print("\t}") >> outFile_i; -} - -function printJavaEnumItem(key, value) { - printf(key "(" value ")") >> outFile_i; -} - -function printJavaEnd() { - - outFile_i = createJavaFileName(myClass); - - print("}") >> outFile_i; - print("") >> outFile_i; - - close(outFile_i); -} - -/^[ \t]*COMMAND_.*$/ { - - doWrapp = !match(($0), /.*COMMAND_NULL.*/); - if (doWrapp) { - #parse enumeration of format x = number1, - sub(",", "", $4); - cmdsTopicNames[cmdsTopicNamesLength] = $2; - cmdsTopicNamesLength++; - cmdsTopicValues[cmdsTopicValuesLength] = $4; - cmdsTopicValuesLength++; - } else { - print("Java-AIInterface: NOTE: JNI level: COMMANDS: intentionally not wrapped: " $2); - } -} - -/^[ \t]*UNIT_COMMAND_.*$/ { - doWrapp = !match(($0), /.*COMMAND_NULL.*/); - if (doWrapp) { - unitCmdsTopicNames[unitCmdsTopicNamesLength] = $2; - unitCmdsTopicNamesLength++; - #parse enumeration of format x = (number1 << number2), - sub("[ \t]*//.*", "", $0); - sub(",", "", $0); - sub($3, "", $0); - sub($2, "", $0); - sub("[ \t]*", "", $0); - unitCmdsTopicValues[unitCmdsTopicValuesLength] = $0; - unitCmdsTopicValuesLength++; - } else { - print("Java-AIInterface: NOTE: JNI level: UNIT_COMMANDS: intentionally not wrapped: " $2); - } -} - -# This function has to return true (1) if a doc comment (eg: /** foo bar */) -# can be deleted. -# If there is no special condition you want to apply, -# it should always return true (1), -# cause there are additional mechanism to prevent accidental deleting. -# see: commonDoc.awk -function canDeleteDocumentation() { - return isInsideEvtStruct != 1; -} - -END { - # finalize things - printJavaHeader(); - printJavaEnums(); - printJavaEnd(); -} diff --git a/AI/Interfaces/Java/bin/jni_wrappEvents.awk b/AI/Interfaces/Java/bin/jni_wrappEvents.awk deleted file mode 100755 index a1907084b30..00000000000 --- a/AI/Interfaces/Java/bin/jni_wrappEvents.awk +++ /dev/null @@ -1,570 +0,0 @@ -#!/usr/bin/awk -f -# -# This awk script creates a Java class with one function per event, -# plus a C wrapper to easily/comfortably call these functions from C. -# input is taken from file: -# rts/ExternalAI/Interface/AISEvents.h -# -# This script uses functions from the following files: -# * common.awk -# * commonDoc.awk -# Variables that can be set on the command-line (with -v): -# * GENERATED_SOURCE_DIR: the generated sources root dir -# -# usage: -# awk -f thisScript.awk -f common.awk -f commonDoc.awk -# awk -f thisScript.awk -f common.awk -f commonDoc.awk \ -# -v 'GENERATED_SOURCE_DIR=/tmp/build/AI/Interfaces/Java/src-generated/main' -# - -BEGIN { - # initialize things - - # define the field splitter(-regex) - FS = "[ \t]+"; - - # Used by other scripts - JAVA_MODE = 1; - - # These vars can be assigned externally, see file header. - # Set the default values if they were not supplied on the command line. - if (!GENERATED_SOURCE_DIR) { - GENERATED_SOURCE_DIR = "../src-generated/main"; - } - if (!JAVA_GENERATED_SOURCE_DIR) { - JAVA_GENERATED_SOURCE_DIR = GENERATED_SOURCE_DIR "/java"; - } - if (!NATIVE_GENERATED_SOURCE_DIR) { - NATIVE_GENERATED_SOURCE_DIR = GENERATED_SOURCE_DIR "/native"; - } - - myWrapperName = "EventsJNIBridge"; - myWrapperPrefix = "eventsJniBridge_"; - - myPkgA = "com.springrts.ai"; - myPkgD = convertJavaNameFormAToD(myPkgA); - myAIClass = "AI"; - myAIAbstractClass = "AbstractAI"; - myAICallbackInt = "AICallback"; - - ind_evtTopics = 0; - ind_evtStructs = 0; - isInsideEvtStruct = 0; -} - - - -# Checks if a field is available and is no comment -function isFieldUsable(f) { - - valid = 0; - - if (f && !match(f, /.*\/\/.*/)) { - valid = 1; - } - - return valid; -} - -function createNativeFileName(fileName_fn, isHeader_fn) { - - absFileName_fn = NATIVE_GENERATED_SOURCE_DIR "/" fileName_fn; - if (isHeader_fn) { - absFileName_fn = absFileName_fn ".h"; - } else { - absFileName_fn = absFileName_fn ".c"; - } - - return absFileName_fn; -} -function createJavaFileName(fileName_fn) { - return JAVA_GENERATED_SOURCE_DIR "/" myPkgD "/" fileName_fn ".java"; -} - - -function printNativeHeader() { - - outFile_nh = createNativeFileName(myWrapperName, 1); - outFile_nc = createNativeFileName(myWrapperName, 0); - printCommentsHeader(outFile_nh); - printCommentsHeader(outFile_nc); - - # print includes (header) - print("") >> outFile_nh; - print("#ifndef __EVENTS_JNI_BRIDGE_H") >> outFile_nh; - print("#define __EVENTS_JNI_BRIDGE_H") >> outFile_nh; - print("") >> outFile_nh; - print("#include ") >> outFile_nh; - print("") >> outFile_nh; - print("#ifdef __cplusplus") >> outFile_nh; - print("extern \"C\" {") >> outFile_nh; - print("#endif") >> outFile_nh; - print("") >> outFile_nh; - - # print includes (source) - print("") >> outFile_nc; - print("#include \"" myWrapperName ".h\"") >> outFile_nc; - print("") >> outFile_nc; - print("#include \"JavaBridge.h\" // for INT_AI") >> outFile_nc; - print("#include \"JniUtil.h\"") >> outFile_nc; - print("#include \"ExternalAI/Interface/AISEvents.h\"") >> outFile_nc; - print("#include // for calloc(), free()") >> outFile_nc; - print("") >> outFile_nc; - print("") >> outFile_nc; - - # print global vars (source) - print("size_t skirmishAIId_size = 0;") >> outFile_nc; - print("jobject* skirmishAIId_callback = NULL;") >> outFile_nc; - print("") >> outFile_nc; -} - -function printNativeEventMethodVars() { - - for (e=0; e < ind_evtStructs; e++) { - printNativeEventMethodVar(e); - } - print("") >> outFile_nc; -} -function printNativeEventMethodVar(evtIndex) { - - outFile_nc = createNativeFileName(myWrapperName, 0); - - eName = evtsName[evtIndex]; - eNameLowerized = lowerize(eName); - - print("jmethodID m_ai_" eNameLowerized " = NULL;") >> outFile_nc; -} - -function printNativeStaticInitFuncHead() { - - # ... header - print("/**") >> outFile_nh; - print(" * Initialized stuff needed for sending events to an AI library instance.") >> outFile_nh; - print(" *") >> outFile_nh; - print(" * @author hoijui") >> outFile_nh; - print(" * @version GENERATED") >> outFile_nh; - print(" */") >> outFile_nh; - print("int " myWrapperPrefix "initStatic(JNIEnv* env, size_t skirmishAIId_size);") >> outFile_nh; - print("") >> outFile_nh; - # ... source - print("int " myWrapperPrefix "initStatic(JNIEnv* env, size_t _skirmishAIId_size) {") >> outFile_nc; - print("") >> outFile_nc; - print("\t" "skirmishAIId_size = _skirmishAIId_size;") >> outFile_nc; - print("\t" "skirmishAIId_callback = (jobject*) calloc(skirmishAIId_size, sizeof(jobject));") >> outFile_nc; - print("\t" "size_t t;") >> outFile_nc; - print("\t" "for (t=0; t < skirmishAIId_size; ++t) {") >> outFile_nc; - print("\t\t" "skirmishAIId_callback[t] = NULL;") >> outFile_nc; - print("\t" "}") >> outFile_nc; - print("") >> outFile_nc; - print("\t" "jobject c_aiInt = (*env)->FindClass(env, \"" myPkgD "/" myAIClass "\");") >> outFile_nc; - print("\t" "if (jniUtil_checkException(env, \"Failed fetching AI interface class " myPkgA "." myAIClass "\")) { return -2; }") >> outFile_nc; - print("") >> outFile_nc; -} - -function printNativeEventStaticInits() { - - for (e=0; e < ind_evtStructs; e++) { - printNativeEventStaticInit(e); - } - print("") >> outFile_nc; -} -function printNativeEventStaticInit(evtIndex) { - - outFile_nc = createNativeFileName(myWrapperName, 0); - - eName = evtsName[evtIndex]; - eNameLowerized = lowerize(eName); - eSignature = "("; - for (m=0; m < evtsNumMembers[evtIndex]; m++) { - type_c = evtsMembers_type_c[evtIndex, m]; - type_jni = convertCToJNIType(type_c); - type_sig = convertJNIToSignatureType(type_jni); - eSignature = eSignature type_sig; - } - eSignature = eSignature ")I"; - if (eNameLowerized == "init") { - eSignature = "(IL" myPkgD "/" myAICallbackInt ";)I"; - } - - print("\t" "m_ai_" eNameLowerized " = jniUtil_getMethodID(env, c_aiInt, \"" eNameLowerized "\", \"" eSignature "\");") >> outFile_nc; - print("\t" "if (jniUtil_checkException(env, \"Failed fetching Java AI method ID for: " eNameLowerized "\")) { return -3; }") >> outFile_nc; - print("") >> outFile_nc; -} - -function printNativeInitFunc() { - - print("\t" "return 0; // -> no error") >> outFile_nc; - print("}") >> outFile_nc; - print("") >> outFile_nc; - - # ... header - print("/**") >> outFile_nh; - print(" * Initialized stuff needed for an AI instance.") >> outFile_nh; - print(" *") >> outFile_nh; - print(" * @author hoijui") >> outFile_nh; - print(" * @version GENERATED") >> outFile_nh; - print(" */") >> outFile_nh; - print("int " myWrapperPrefix "initAI(JNIEnv* env, int skirmishAIId, jobject callback);") >> outFile_nh; - print("") >> outFile_nh; - # ... source - print("int " myWrapperPrefix "initAI(JNIEnv* env, int skirmishAIId, jobject callback) {") >> outFile_nc; - print("") >> outFile_nc; - print("\t" "int res = -1;") >> outFile_nc; - print("") >> outFile_nc; - print("\t" "skirmishAIId_callback[skirmishAIId] = callback;") >> outFile_nc; - print("\t" "res = 0;") >> outFile_nc; - print("") >> outFile_nc; - print("\t" "return res;") >> outFile_nc; - print("}") >> outFile_nc; - print("") >> outFile_nc; -} - -function printNativeHandleFuncHead() { - - # ... header - print("/**") >> outFile_nh; - print(" * For documentation, see SSkirmishAILibrary::handleEvent() in:") >> outFile_nh; - print(" * ExternalAI/Interface/SSkirmishAILibrary.h") >> outFile_nh; - print(" *") >> outFile_nh; - print(" * @author hoijui") >> outFile_nh; - print(" * @version GENERATED") >> outFile_nh; - print(" */") >> outFile_nh; - print("int " myWrapperPrefix "handleEvent(JNIEnv* env, jobject aiInstance, int skirmishAIId, int topic, const void* data);") >> outFile_nh; - print("") >> outFile_nh; - # ... source - print("int " myWrapperPrefix "handleEvent(JNIEnv* env, jobject aiInstance, int skirmishAIId, int topic, const void* data) {") >> outFile_nc; - print("") >> outFile_nc; - print("\t" "int _ret = -1;") >> outFile_nc; - print("") >> outFile_nc; -# print("\t" "jobject o_ai = skirmishAIId_aiObject[skirmishAIId];") >> outFile_nc; -# print("\t" "//assert(o_ai != NULL);") >> outFile_nc; -# print("") >> outFile_nc; - - # only add this for debugging purposes -# print("\t" "// in case we missed handling an exception elsewhere") >> outFile_nc; -# print("\t" "// and it is therefore still pending, we handle it here") >> outFile_nc; -# print("\t" "if ((*env)->ExceptionCheck(env)) {") >> outFile_nc; -# print("\t\t" "(*env)->ExceptionDescribe(env);") >> outFile_nc; -# print("\t\t" "_ret = -6;") >> outFile_nc; -# print("\t\t" "return _ret;") >> outFile_nc; -# print("\t" "}") >> outFile_nc; -# print("") >> outFile_nc; - - print("\t" "switch (topic) {") >> outFile_nc; -} - - -function printNativeEventCases() { - - for (e=0; e < ind_evtStructs; e++) { - printNativeEventCase(e); - } -} -function printNativeEventCase(evtIndex) { - - outFile_nc = createNativeFileName(myWrapperName, 0); - - topicName = evtsTopicName[evtIndex]; - topicValue = evtsTopicNameValue[topicName]; - eName = evtsName[evtIndex]; - eNameLowerized = lowerize(eName); - eStruct = "S" eName "Event"; - - print("\t\t" "case " topicName ": {") >> outFile_nc; - print("\t\t\t" "const struct " eStruct "* evt = (const struct "eStruct"*) data;") >> outFile_nc; - - paramsEvt = ""; - conversion_pre = ""; - for (m=0; m < evtsNumMembers[evtIndex]; m++) { - type_c = evtsMembers_type_c[evtIndex, m]; - name_c = evtsMembers_name[evtIndex, m]; - value_c = "evt->" name_c; - name_jni = value_c; - - type_jni = convertCToJNIType(type_c); - if (type_jni == "jstring") { - name_jni = name_c "_jni"; - conversion_pre = conversion_pre "\n\t\t\t" type_jni " " name_jni " = (*env)->NewStringUTF(env, " value_c ");"; - } else if (type_jni == "jfloatArray") { - name_jni = name_c "_jni"; - - array_size = "sizeof(" value_c ")"; - if (match(name_c, /_posF3$/)) { - array_size = "3"; - } - - conversion_pre = conversion_pre "\n\t\t\t" type_jni " " name_jni " = (*env)->NewFloatArray(env, " array_size ");"; - conversion_pre = conversion_pre "\n\t\t\t" "(*env)->SetFloatArrayRegion(env, " name_jni ", 0, " array_size ", " value_c ");"; - } else if (type_jni == "jintArray") { - name_jni = name_c "_jni"; - - array_size = "sizeof(" value_c ")"; - - conversion_pre = conversion_pre "\n\t\t\t" type_jni " " name_jni " = (*env)->NewIntArray(env, " array_size ");"; - conversion_pre = conversion_pre "\n\t\t\t" "(*env)->SetIntArrayRegion(env, " name_jni ", 0, " array_size ", " value_c ");"; - } - - paramsEvt = paramsEvt ", " name_jni; - } - sub(/^, /, "", paramsEvt); - if (eNameLowerized == "init") { - sub(/evt->callback/, "skirmishAIId_callback[evt->skirmishAIId]", paramsEvt); - } - - print(conversion_pre) >> outFile_nc; - print("\t\t\t" "_ret = (*env)->CallIntMethod(env, aiInstance, m_ai_" eNameLowerized ", " paramsEvt ");") >> outFile_nc; - - print("\t\t\t" "break;") >> outFile_nc; - print("\t\t" "}") >> outFile_nc; -} - -function printNativeEnd() { - - outFile_nh = createNativeFileName(myWrapperName, 1); - outFile_nc = createNativeFileName(myWrapperName, 0); - - print("#ifdef __cplusplus") >> outFile_nh; - print("} // extern \"C\"") >> outFile_nh; - print("#endif") >> outFile_nh; - print("") >> outFile_nh; - print("#endif // __EVENTS_JNI_BRIDGE_H") >> outFile_nh; - print("") >> outFile_nh; - - print("\t\t" "default: {") >> outFile_nc; - print("\t\t\t" "_ret = -4;") >> outFile_nc; - print("\t\t\t" "break;") >> outFile_nc; - print("\t\t" "}") >> outFile_nc; - print("\t" "}") >> outFile_nc; - print("") >> outFile_nc; - print("\t" "if ((*env)->ExceptionCheck(env)) {") >> outFile_nc; - print("\t\t" "(*env)->ExceptionDescribe(env);") >> outFile_nc; - print("\t\t" "_ret = -5;") >> outFile_nc; - print("\t" "}") >> outFile_nc; - print("") >> outFile_nc; - print("\t" "return _ret;") >> outFile_nc; - print("}") >> outFile_nc; - - close(outFile_nh); - close(outFile_nc); -} - - -function printGeneralJavaHeader(outFile_h, javaPkg_h, javaClassName_h) { - - printCommentsHeader(outFile_h); - print("") >> outFile_h; - print("package " javaPkg_h ";") >> outFile_h; - print("") >> outFile_h; - print("") >> outFile_h; - print("/**") >> outFile_h; - print(" * This is the Java entry point from events coming from the engine.") >> outFile_h; - print(" * We are using JNI for best in speed.") >> outFile_h; - print(" *") >> outFile_h; - print(" * @author AWK wrapper script") >> outFile_h; - print(" * @version GENERATED") >> outFile_h; - print(" */") >> outFile_h; - if (javaClassName_h == myAIAbstractClass) { - print("public abstract class " javaClassName_h " implements " myAIClass " {") >> outFile_h; - } else { - print("public interface " javaClassName_h " {") >> outFile_h; - } - print("") >> outFile_h; -} - -function printJavaHeader() { - - outFile_i = createJavaFileName(myAIClass); - outFile_a = createJavaFileName(myAIAbstractClass); - - printGeneralJavaHeader(outFile_i, myPkgA, myAIClass); - printGeneralJavaHeader(outFile_a, myPkgA, myAIAbstractClass); -} - -function printJavaEvents() { - - for (e=0; e < ind_evtStructs; e++) { - printJavaEvent(e); - } -} -function printJavaEvent(evtIndex) { - - outFile_i = createJavaFileName(myAIClass); - outFile_a = createJavaFileName(myAIAbstractClass); - - topicName = evtsTopicName[evtIndex]; - topicValue = evtsTopicNameValue[topicName]; - eName = evtsName[evtIndex]; - eMetaComment = evtsMetaComment[evtIndex]; - eNameLowerized = lowerize(eName); - eCls = eName "AIEvent"; - - # Add the member comments to the main comment as @param attributes - numEvtDocLines = evtsDocComment[evtIndex, "*"]; - for (m=firstMember; m < evtsNumMembers[evtIndex]; m++) { - numLines = evtMbrsDocComments[evtIndex*1000 + m, "*"]; - if (numLines > 0) { - evtsDocComment[evtIndex, numEvtDocLines] = "@param " evtsMembers_name[evtIndex, m]; - } - for (l=0; l < numLines; l++) { - if (l == 0) { - _preDocLine = "@param " evtsMembers_name[evtIndex, m] " "; - } else { - _preDocLine = " " lengtAsSpaces(evtsMembers_name[evtIndex, m]) " "; - } - evtsDocComment[evtIndex, numEvtDocLines] = _preDocLine evtMbrsDocComments[evtIndex*1000 + m, l]; - numEvtDocLines++; - } - } - evtsDocComment[evtIndex, "*"] = numEvtDocLines; - - paramsTypes = ""; - for (m=0; m < evtsNumMembers[evtIndex]; m++) { - name_c = evtsMembers_name[evtIndex, m]; - type_c = evtsMembers_type_c[evtIndex, m]; - type_java = convertJNIToJavaType(convertCToJNIType(type_c)); - - paramsTypes = paramsTypes ", " type_java " " name_c; - } - sub(/^, /, "", paramsTypes); - if (eNameLowerized == "init") { - paramsTypes = "int skirmishAIId, AICallback callback"; - } - - _condMetaComments = eMetaComment; - if (_condMetaComments != "") { - _condMetaComments = " // " _condMetaComments; - } - - print("") >> outFile_i; - printFunctionComment_Common(outFile_i, evtsDocComment, evtIndex, "\t"); - print("\t" "public int " eNameLowerized "(" paramsTypes ");" _condMetaComments) >> outFile_i; - - print("") >> outFile_a; - print("\t" "@Override") >> outFile_a; - print("\t" "public int " eNameLowerized "(" paramsTypes ") {") >> outFile_a; - print("") >> outFile_a; - print("\t\t" "// signal: event handled OK") >> outFile_a; - print("\t\t" "return 0;") >> outFile_a; - print("\t" "}") >> outFile_a; -} - -function printJavaEnd() { - - outFile_i = createJavaFileName(myAIClass); - outFile_a = createJavaFileName(myAIAbstractClass); - - print("}") >> outFile_i; - print("") >> outFile_i; - - print("}") >> outFile_a; - print("") >> outFile_a; - - close(outFile_i); - close(outFile_a); -} - - -function saveMember(ind_mem_s, member_s) { - - name_s = extractParamName(member_s); - type_c_s = extractCType(member_s); - - evtsMembers_name[ind_evtStructs, ind_mem_s] = name_s; - evtsMembers_type_c[ind_evtStructs, ind_mem_s] = type_c_s; -} - - -# aggare te los event defines in order -/^[ \t]*EVENT_.*$/ { - - doWrapp = !match(($0), /.*EVENT_NULL.*/) && !match(($0), /.*EVENT_TO_ID_ENGINE.*/); - if (doWrapp) { - sub(",", "", $4); - evtsTopicNameValue[$2] = $4; - } else { - print("Java-AIInterface: NOTE: JNI level: Events: intentionally not wrapped: " $2); - } -} - - -# This function has to return true (1) if a doc comment (eg: /** foo bar */) -# can be deleted. -# If there is no special condition you want to apply, -# it should always return true (1), -# cause there are additional mechanism to prevent accidental deleting. -# see: commonDoc.awk -function canDeleteDocumentation() { - return isInsideEvtStruct != 1; -} - -################################################################################ -### BEGIN: parsing and saving the event structs - -# end of struct S*Event -/^}; \/\/\$ EVENT_/ { - - evtsNumMembers[ind_evtStructs] = ind_evtMember; - evtsTopicName[ind_evtStructs] = $3; - _metaComment = $0; - sub("^.*" evtsTopicName[ind_evtStructs], "", _metaComment); - evtsMetaComment[ind_evtStructs] = trim(_metaComment); - - ind_evtStructs++; - isInsideEvtStruct = 0; -} - - -# inside of struct S*Event -{ - if (isInsideEvtStruct == 1) { - size_tmpMembers = split($0, tmpMembers, ";"); - # cause there is an empty part behind the ';' - size_tmpMembers--; - for (i=1; i<=size_tmpMembers; i++) { - tmpMembers[i] = trim(tmpMembers[i]); - if (tmpMembers[i] == "" || match(tmpMembers[i], /^\/\//)) { - break; - } - # This would bork with more then 1000 members in an event, - # or more then 1000 events - storeDocLines(evtMbrsDocComments, ind_evtStructs*1000 + ind_evtMember); - saveMember(ind_evtMember, tmpMembers[i]); - ind_evtMember++; - } - } -} - -# beginning of struct S*Event -/^struct S.*Event( \{)?/ { - - isInsideEvtStruct = 1; - ind_evtMember = 0; - eventName = $2; - sub(/^S/, "", eventName); - sub(/Event$/, "", eventName); - - evtsName[ind_evtStructs] = eventName; - storeDocLines(evtsDocComment, ind_evtStructs); -} - -### END: parsing and saving the event structs -################################################################################ - - - - -END { - # finalize things - printNativeHeader(); - printNativeEventMethodVars(); - printNativeStaticInitFuncHead(); - printNativeEventStaticInits(); - printNativeInitFunc(); - printNativeHandleFuncHead(); - printNativeEventCases(); - printNativeEnd(); - - printJavaHeader(); - printJavaEvents(); - printJavaEnd(); -} diff --git a/AI/Interfaces/Java/bin/native_createCallbackFPInitializations.awk b/AI/Interfaces/Java/bin/native_createCallbackFPInitializations.awk deleted file mode 100755 index c6aed9bdeef..00000000000 --- a/AI/Interfaces/Java/bin/native_createCallbackFPInitializations.awk +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/awk -f -# -# This awk script creates the function pointer initializations -# for the C callbacks; eg: -# rts/ExternalAI/Interface/SSkirmishAICallback.h -# rts/ExternalAI/Interface/SAIInterfaceCallback.h -# - -BEGIN { - # initialize things - - # define the field splitter(-regex) - #FS="[ \t]+" -} - - -# Some utility functions - -function ltrim(s) { sub(/^[ \t]+/, "", s); return s; } -function rtrim(s) { sub(/[ \t]+$/, "", s); return s; } -function trim(s) { return rtrim(ltrim(s)); } - - -function printInit(functionPointerName) { - - functionName = functionPointerName; - sub(/Clb_/, "skirmishAiCallback_", functionName); - - print(" callback->" functionPointerName " = &" functionName ";") -} - - -/^.*CALLING_CONV.*$/ { - - line = trim($0); - doWrapp = !match(line, /^[ \t]*\/\/.*/); - if (doWrapp) { - numParts = split(line, parts, /\(CALLING_CONV \*/); - if (numParts == 2) { - numParts2 = split(parts[2], parts2, /\)\(/); - if (numParts2 == 2) { - fn = parts2[1]; - printInit(fn); - } - } - } -} - - - -END { - # finalize things -} diff --git a/AI/Interfaces/Java/bin/native_createCallbackFunctionImpls.awk b/AI/Interfaces/Java/bin/native_createCallbackFunctionImpls.awk deleted file mode 100755 index c9a789ab005..00000000000 --- a/AI/Interfaces/Java/bin/native_createCallbackFunctionImpls.awk +++ /dev/null @@ -1,124 +0,0 @@ -#!/usr/bin/awk -f -# -# This awk script creates function impls that can be used in the C callbacks -# with some little adjustments; eg: -# rts/ExternalAI/SSkirmishAICallbackImpl.cpp -# rts/ExternalAI/SAIInterfaceCallbackImpl.cpp -# -# Accepts input like this: -# [code] -# bool requireSonarUnderWater; -# -# // construction related fields -# /// Should constructions without builders decay? -# bool constructionDecay; -# [/code] -# -# use like this: -# awk -f yourScript.awk -f common.awk -f commonDoc.awk [additional-params] -# this should work with all flavours of AWK (eg. gawk, mawk, nawk, ...) -# - -BEGIN { - # initialize things - - # define the field splitter(-regex) - #FS="[ \t]+" - - # 0 -> print impl - # 1 -> print impl header - # 2 -> print interface header - printingHeader = 1; -} - -function getCType(gct_type) { - - if (match(gct_type, /string/)) { - return "const char* const"; - } else { - return gct_type; - } -} -function getToCTypeConv(gtctc_type) { - - if (match(gtctc_type, /string/)) { - return ".c_str()"; - } else { - return ""; - } -} -function getFuncPrefix(gfp_type) { - - #if (match(gfp_type, /bool/)) { - # return "is"; - #} else { - return "get"; - #} -} - -function printFunc(pf_fieldName, pf_type, pf_header) { - - pf_cType = getCType(pf_type); - pf_toCTypeConv = getToCTypeConv(pf_type); - pf_funcPrefix = getFuncPrefix(pf_type); - - if (pf_header == 2 && hasStoredDoc()) { - printStoredDoc(""); - } - - if (pf_header != 0) { - pf_firstLineEnd = ";"; - pf_sizeToFill = length("const char* const") - length(pf_cType); - if (pf_sizeToFill < 0) { - pf_sizeToFill = 0; - } - pf_filler = ""; - for (pf_i = 0; pf_i < pf_sizeToFill; pf_i++) { - pf_filler = pf_filler " "; - } - } else { - pf_firstLineEnd = " {"; - pf_filler = ""; - } - - if (pf_header == 2) { - print(pf_cType pf_filler " (CALLING_CONV *PreFixName_" pf_funcPrefix capitalize(pf_fieldName) ")(int teamId)" pf_firstLineEnd); - } else { - print("EXPORT(" pf_cType pf_filler ") PreFixName_" pf_funcPrefix capitalize(pf_fieldName) "(int teamId)" pf_firstLineEnd); - } - if (!pf_header) { - print("\t" "return modInfo->" pf_fieldName pf_toCTypeConv ";"); - print("}"); - } -} - -# needed by commonDoc.awk -function canDeleteDocumentation() { - return 1; -} - - -{ - line = trim($0); - - if (line == "") { - print(line); - } else if (!isInsideDoc()) { - sub(/\;$/, "", line); - - name = line; - sub(/^.*[ \t]/, "", name); - - cppType = line; - sub(name, "", cppType); - cppType = rtrim(cppType); - - printFunc(name, cppType, printingHeader); - } -} - - - -END { - # finalize things -} diff --git a/AI/Interfaces/Java/bin/native_wrappCallback.awk b/AI/Interfaces/Java/bin/native_wrappCallback.awk deleted file mode 100755 index b920c196a6e..00000000000 --- a/AI/Interfaces/Java/bin/native_wrappCallback.awk +++ /dev/null @@ -1,290 +0,0 @@ -#!/usr/bin/awk -f -# -# This awk script creates C functions which call C function pointers in: -# rts/ExternalAI/Interface/SSkirmishAICallback.h -# -# Right after running this script, you have to wrap the native command structs -# into functions. -# -# This script uses functions from the following files: -# * common.awk -# * commonDoc.awk -# Variables that can be set on the command-line (with -v): -# * GENERATED_SOURCE_DIR the root generated sources dir -# default: "../src-generated/main" -# * NATIVE_GENERATED_SOURCE_DIR the native generated sources dir -# default: GENERATED_SOURCE_DIR + "/native" -# -# usage: -# awk -f thisScript.awk -f common.awk -f commonDoc.awk -# awk -f thisScript.awk -f common.awk -f commonDoc.awk \ -# -v 'NATIVE_GENERATED_SOURCE_DIR=/tmp/build/AI/Interfaces/Java/src-generated/main/native' -# - -BEGIN { - # initialize things - - # define the field splitter(-regex) - FS = "(,)|(\\()|(\\);)"; - - # These vars can be assigned externally, see file header. - # Set the default values if they were not supplied on the command line. - if (!GENERATED_SOURCE_DIR) { - GENERATED_SOURCE_DIR = "../src-generated/main"; - } - if (!NATIVE_GENERATED_SOURCE_DIR) { - NATIVE_GENERATED_SOURCE_DIR = GENERATED_SOURCE_DIR "/native"; - } - - nativeBridge = "CallbackFunctionPointerBridge"; - bridgePrefix = "bridged__"; - - fi = 0; -} - -function doWrapp(funcIndex_dw) { - - paramListC_dw = funcParamListC[funcIndex_dw]; - doWrapp_dw = 1; - - fullName_dw = funcFullName[funcIndex_dw]; - if (doWrapp_dw) { - metaInf_dw = funcMetaInf[funcIndex_dw]; - - if (match(metaInf_dw, /ARRAY:/)) { - #doWrapp_dw = 0; - } - if (match(metaInf_dw, /MAP:/)) { - #doWrapp_dw = 0; - } - if (match(fullName_dw, "^" bridgePrefix "File_")) { - doWrapp_dw = 0; - } - if (fullName_dw == "Engine_handleCommand") { - doWrapp_dw = 0; - } - # not wrapped, legacy C++ only - if (fullName_dw == "Engine_executeCommand") { - doWrapp_dw = 0; - } - } else { - print("Java-AIInterface: NOTE: native level: Callback: intentionally not wrapped: " fullName_dw); - } - - return doWrapp_dw; -} - -function createNativeFileName(fileName_fn, isHeader_fn) { - - absFileName_fn = NATIVE_GENERATED_SOURCE_DIR "/" fileName_fn; - if (isHeader_fn) { - absFileName_fn = absFileName_fn ".h"; - } else { - absFileName_fn = absFileName_fn ".c"; - } - - return absFileName_fn; -} - -function printNativeFP2F() { - - outFile_nh = createNativeFileName(nativeBridge, 1); - outFile_nc = createNativeFileName(nativeBridge, 0); - - printCommentsHeader(outFile_nh); - printCommentsHeader(outFile_nc); - - print("") >> outFile_nh; - print("#ifndef __CALLBACK_FUNCTION_POINTER_BRIDGE_H") >> outFile_nh; - print("#define __CALLBACK_FUNCTION_POINTER_BRIDGE_H") >> outFile_nh; - print("") >> outFile_nh; - print("#include \"ExternalAI/Interface/aidefines.h\"") >> outFile_nh; - print("") >> outFile_nh; - print("#include // size_t") >> outFile_nh; - print("#include // bool, true, false") >> outFile_nh; - print("") >> outFile_nh; - print("struct SSkirmishAICallback;") >> outFile_nh; - print("") >> outFile_nh; - print("#ifdef __cplusplus") >> outFile_nh; - print("extern \"C\" {") >> outFile_nh; - print("#endif") >> outFile_nh; - print("") >> outFile_nh; - print("void funcPntBrdg_addCallback(const size_t skirmishAIId, const struct SSkirmishAICallback* clb);") >> outFile_nh; - print("void funcPntBrdg_removeCallback(const size_t skirmishAIId);") >> outFile_nh; - print("") >> outFile_nh; - - print("") >> outFile_nc; - print("#include \"" nativeBridge ".h\"") >> outFile_nc; - print("") >> outFile_nc; - print("#include \"ExternalAI/Interface/SSkirmishAICallback.h\"") >> outFile_nc; - print("#include \"ExternalAI/Interface/AISCommands.h\"") >> outFile_nc; - print("") >> outFile_nc; - print("") >> outFile_nc; - print("#define id_clb_sizeMax 8192") >> outFile_nc; - print("static const struct SSkirmishAICallback* id_clb[id_clb_sizeMax];") >> outFile_nc; - print("") >> outFile_nc; - print("void funcPntBrdg_addCallback(const size_t skirmishAIId, const struct SSkirmishAICallback* clb) {") >> outFile_nc; - print(" //assert(skirmishAIId < id_clb_sizeMax);") >> outFile_nc; - print(" id_clb[skirmishAIId] = clb;") >> outFile_nc; - print("}") >> outFile_nc; - print("void funcPntBrdg_removeCallback(const size_t skirmishAIId) {") >> outFile_nc; - print(" //assert(skirmishAIId < id_clb_sizeMax);") >> outFile_nc; - print(" id_clb[skirmishAIId] = NULL;") >> outFile_nc; - print("}") >> outFile_nc; - print("") >> outFile_nc; - - # print the wrapping functions - for (i=0; i < fi; i++) { - fullName = funcFullName[i]; - retType = funcRetTypeC[i]; - paramList = funcParamListC[i]; - sub(/int skirmishAIId/, "int _skirmishAIId", paramList); - paramListNoTypes = removeParamTypes(paramList); - commentEol = funcCommentEol[i]; - - if (doWrapp(i)) { - # print function declaration to *.h - printFunctionComment_Common(outFile_nh, funcDocComment, i, ""); - - outName = bridgePrefix fullName; - - if (commentEol != "") { - commentEol = " // " commentEol; - } - print("EXPORT(" retType ") " outName "(" paramList ");" commentEol) >> outFile_nh; - print("") >> outFile_nh; - - # print function definition to *.c - print("EXPORT(" retType ") " outName "(" paramList ") {") >> outFile_nc; - - print(preConversion) >> outFile_nc; - if (hasRetParam) { - print("\t" retParamType " " retNameTmp " = id_clb[_skirmishAIId]->" fullName "(" paramListNoTypes ");") >> outFile_nc; - print(retParamConversion) >> outFile_nc; - } else { - condRet = "return "; - if (retType == "void") { - condRet = ""; - } - print("\t" condRet "id_clb[_skirmishAIId]->" fullName "(" paramListNoTypes ");") >> outFile_nc; - } - print("" "}") >> outFile_nc; - } else { - print("Note: The following function is intentionally not wrapped: " fullName); - } - } - - - print("") >> outFile_nh; - print("") >> outFile_nc; - - close(outFile_nh); - close(outFile_nc); -} - - -function wrappFunction(funcDef, commentEolTot) { - - doParse = 1; # add a function in case you want to exclude some - - if (doParse) { - size_funcParts = split(funcDef, funcParts, "(,)|(\\()|(\\);)"); - # because the empty part after ");" would count as part as well - size_funcParts--; - retType_c = trim(funcParts[1]); - fullName = funcParts[2]; - sub(/CALLING_CONV \*/, "", fullName); - sub(/\)/, "", fullName); - - # function parameters - paramList_c = ""; - paramList = ""; - - for (i=3; i<=size_funcParts && !match(funcParts[i], /.*\/\/.*/); i++) { - type_c = extractParamType(funcParts[i]); - type_c = cleanupCType(type_c); - name = extractParamName(funcParts[i]); - if (i == 3) { - cond_comma = ""; - } else { - cond_comma = ", "; - } - paramList_c = paramList_c cond_comma type_c " " name; - } - - funcFullName[fi] = fullName; - funcRetTypeC[fi] = retType_c; - funcParamListC[fi] = paramList_c; - funcParamList[fi] = paramList; - funcCommentEol[fi] = trim(commentEolTot); - storeDocLines(funcDocComment, fi); - fi++; - } else { - print("warninig: function intentionally NOT wrapped: " funcDef); - } -} - - - -# This function has to return true (1) if a doc comment (eg: /** foo bar */) -# can be deleted. -# If there is no special condition you want to apply, -# it should always return true (1), -# cause there are additional mechanism to prevent accidental deleting. -# see: commonDoc.awk -function canDeleteDocumentation() { - return isMultiLineFunc != 1; -} - - - -# save function pointer info into arrays -# ... 2nd, 3rd, ... line of a function pointer definition -{ - if (isMultiLineFunc) { # function is defined on one single line - funcIntermLine = $0; - # separate possible comment at end of line: //$ foo bar - commentEol = funcIntermLine; - if (sub(/.*\/\/\$/, "", commentEol)) { - commentEolTot = commentEolTot commentEol; - } - # remove possible comment at end of line: //$ foo bar - sub(/[ \t]*\/\/.*/, "", funcIntermLine); - funcIntermLine = trim(funcIntermLine); - funcSoFar = funcSoFar " " funcIntermLine; - if (match(funcSoFar, /;$/)) { - # function ends in this line - wrappFunction(funcSoFar, commentEolTot); - isMultiLineFunc = 0; - } - } -} -# 1st line of a function pointer definition -/^[^\/]*CALLING_CONV.*$/ { - - funcStartLine = $0; - # separate possible comment at end of line: //$ foo bar - commentEolTot = ""; - commentEol = funcStartLine; - if (sub(/.*\/\/\$/, "", commentEol)) { - commentEolTot = commentEolTot commentEol; - } - # remove possible comment at end of line: //$ foo bar - sub(/\/\/.*$/, "", funcStartLine); - funcStartLine = trim(funcStartLine); - if (match(funcStartLine, /;$/)) { - # function ends in this line - wrappFunction(funcStartLine, commentEolTot); - } else { - funcSoFar = funcStartLine; - isMultiLineFunc = 1; - } -} - - - -END { - # finalize things - - printNativeFP2F(); -} diff --git a/AI/Interfaces/Java/bin/native_wrappCommands.awk b/AI/Interfaces/Java/bin/native_wrappCommands.awk deleted file mode 100755 index 9de74c4a95f..00000000000 --- a/AI/Interfaces/Java/bin/native_wrappCommands.awk +++ /dev/null @@ -1,384 +0,0 @@ -#!/usr/bin/awk -f -# -# This awk script creates the C functions for wrapping the C command structs in: -# rts/ExternalAI/Interface/AISCommands.h -# -# Before running this script, you have to wrap the native callback struct -# into functions. -# -# This script uses functions from the following files: -# * common.awk -# * commonDoc.awk -# Variables that can be set on the command-line (with -v): -# * GENERATED_SOURCE_DIR the root generated sources dir -# default: "../src-generated/main" -# * NATIVE_GENERATED_SOURCE_DIR the native generated sources dir -# default: GENERATED_SOURCE_DIR + "/native" -# -# usage: -# awk -f thisScript.awk -f common.awk -f commonDoc.awk -# awk -f thisScript.awk -f common.awk -f commonDoc.awk \ -# -v 'NATIVE_GENERATED_SOURCE_DIR=/tmp/build/AI/Interfaces/Java/src-generated/main/native' -# - -BEGIN { - # initialize things - - # define the field splitter(-regex) - FS = "[ \t]+"; - - # These vars can be assigned externally, see file header. - # Set the default values if they were not supplied on the command line. - if (!GENERATED_SOURCE_DIR) { - GENERATED_SOURCE_DIR = "../src-generated/main"; - } - if (!NATIVE_GENERATED_SOURCE_DIR) { - NATIVE_GENERATED_SOURCE_DIR = GENERATED_SOURCE_DIR "/native"; - } - - nativeBridge = "CallbackFunctionPointerBridge"; - bridgePrefix = "bridged__"; - - indent = " "; - - ind_cmdTopics = 0; - ind_cmdStructs = 0; - insideCmdStruct = 0; -} - - -# Checks if a field is available and is no comment -function isFieldUsable(f) { - - valid = 0; - - if (f && !match(f, /.*\/\/.*/)) { - valid = 1; - } - - return valid; -} - - - - -function saveMember(ind_mem, member) { - - name = extractParamName(member); - type_c = extractCType(member); - - cmdsMembers_name[ind_cmdStructs, ind_mem] = name; - cmdsMembers_type_c[ind_cmdStructs, ind_mem] = type_c; -} - - -function doWrapp(ind_cmdStructs_dw) { - - doWrp_dw = 1; - - if (match(cmdsName[ind_cmdStructs_dw], /SharedMemArea/)) { - doWrp_dw = 0; - } else if (match(cmdsName[ind_cmdStructs_dw], /^SCallLuaRulesCommand$/)) { - doWrp_dw = 0; - } - - return doWrp_dw; -} - - -function createNativeFileName(fileName_fn, isHeader_fn) { - - absFileName_fn = NATIVE_GENERATED_SOURCE_DIR "/" fileName_fn; - if (isHeader_fn) { - absFileName_fn = absFileName_fn ".h"; - } else { - absFileName_fn = absFileName_fn ".c"; - } - - return absFileName_fn; -} - - -function printNativeFP2F() { - - outFile_nh = createNativeFileName(nativeBridge, 1); - outFile_nc = createNativeFileName(nativeBridge, 0); - - print("// END: COMMAND_WRAPPERS") >> outFile_nh; - print("") >> outFile_nh; - print("") >> outFile_nc; - - # print the command wrapping functions - for (cmdIndex=0; cmdIndex < ind_cmdStructs; cmdIndex++) { - topicName = cmdsTopicName[cmdIndex]; - topicValue = cmdsTopicNameValue[topicName]; - name = cmdsName[cmdIndex]; - metaInf = cmdsMetaInfo[cmdIndex]; - fullName = metaInf; - sub(/ .*$/, "", fullName); - sub(/^[^ \t]*/, "", metaInf); - - hasRetType = 0; - retType = "int"; - retParam = ""; - paramList = "int _skirmishAIId"; - firstMember = 0; - if (cmdsNumMembers[cmdIndex] > 0) { - for (m=firstMember; m < cmdsNumMembers[cmdIndex]; m++) { - memName = cmdsMembers_name[cmdIndex, m]; - memType_c = cmdsMembers_type_c[cmdIndex, m]; - - if (match(memName, /^ret_/) && !match(memType_c, /\*/)) { - retParam = memName; - retType = memType_c; - hasRetType = 1; - # rewrite the meta info - sub(memName, "RETURN", metaInf); - } else { - paramList = paramList ", " memType_c " " memName; - } - } - } - if (!hasRetType) { - metaInf = metaInf " error-return:0=OK"; - } - paramListNoTypes = removeParamTypes(paramList); - metaInf = trim(metaInf); - - - if (doWrapp(cmdIndex)) { - # print function declaration to *.h - - # Add the member comments to the main comment as @param attributes - numCmdDocLines = cmdsDocComment[cmdIndex, "*"]; - for (m=firstMember; m < cmdsNumMembers[cmdIndex]; m++) { - numLines = cmdMbrsDocComments[cmdIndex*1000 + m, "*"]; - if (numLines > 0) { - cmdsDocComment[cmdIndex, numCmdDocLines] = "@param " cmdsMembers_name[cmdIndex, m]; - } - for (l=0; l < numLines; l++) { - if (l == 0) { - _preDocLine = "@param " cmdsMembers_name[cmdIndex, m] " "; - } else { - _preDocLine = " " lengtAsSpaces(cmdsMembers_name[cmdIndex, m]) " "; - } - cmdsDocComment[cmdIndex, numCmdDocLines] = _preDocLine cmdMbrsDocComments[cmdIndex*1000 + m, l]; - numCmdDocLines++; - } - } - cmdsDocComment[cmdIndex, "*"] = numCmdDocLines; - - outName = bridgePrefix fullName; - - commentEol = ""; - if (metaInf != "") { - commentEol = " // " metaInf; - } - - if (match(fullName, /^Unit_/)) { - # To make this fit in smoothly with the OO structure, - # we want to present each unit command once for the unit class - # and once for the Group class. - - # An other thing we do, is move the common UnitAICommand params - # to the end of the params list, so we can supply default values - # for them more easily later on. - paramList_commonEnd = paramList; - sub(/, short options, int timeOut/, "", paramList_commonEnd); - paramList_commonEnd = paramList_commonEnd ", short options, int timeOut"; - - # Unit version: - paramList_unit = paramList_commonEnd; - sub(/int groupId, /, "", paramList_unit); - printFunctionComment_Common(outFile_nh, cmdsDocComment, cmdIndex, ""); - print("EXPORT(" retType ") " outName "(" paramList_unit ");" commentEol) >> outFile_nh; - print("") >> outFile_nh; - - # Group version: - paramList_group = paramList_commonEnd; - sub(/int unitId, /, "", paramList_group); - outName_group = outName; - sub(/Unit_/, "Group_", outName_group); - printFunctionComment_Common(outFile_nh, cmdsDocComment, cmdIndex, ""); - print("EXPORT(" retType ") " outName_group "(" paramList_group ");" commentEol) >> outFile_nh; - } else { - printFunctionComment_Common(outFile_nh, cmdsDocComment, cmdIndex, ""); - print("EXPORT(" retType ") " outName "(" paramList ");" commentEol) >> outFile_nh; - } - print("") >> outFile_nh; - - # print function definition to *.c - print("") >> outFile_nc; - if (match(fullName, /^Unit_/)) { - # inner version: - print("static " retType " _" outName "(" paramList ") {") >> outFile_nc; - } else { - print("EXPORT(" retType ") " outName "(" paramList ") {") >> outFile_nc; - } - print("") >> outFile_nc; - - print("\t" "struct S" name "Command commandData;") >> outFile_nc; - for (m=firstMember; m < cmdsNumMembers[cmdIndex]; m++) { - memName = cmdsMembers_name[cmdIndex, m]; - memType_c = cmdsMembers_type_c[cmdIndex, m]; - - if (memName == retParam) { - # do nothing - } else { - print("\t" "commandData." memName " = " memName ";") >> outFile_nc; - } - } - print("") >> outFile_nc; - - print("\t" "int _ret = id_clb[_skirmishAIId]->Engine_handleCommand(_skirmishAIId, COMMAND_TO_ID_ENGINE, -1, " topicName ", &commandData);") >> outFile_nc; - print("") >> outFile_nc; - - if (hasRetType) { - # this is unused, delete - print("\t" "if (_ret == 0) {") >> outFile_nc; - print("\t\t" "_ret = commandData." retParam ";") >> outFile_nc; - print("\t" "} else {") >> outFile_nc; - print("\t\t" "_ret = 0;") >> outFile_nc; - print("\t" "}") >> outFile_nc; - } - - print("\t" "return _ret;") >> outFile_nc; - print("}") >> outFile_nc; - - if (match(fullName, /^Unit_/)) { - paramListNoTypes = removeParamTypes(paramList); - - # Unit version: - print("") >> outFile_nc; - print("EXPORT(" retType ") " outName "(" paramList_unit ") {" commentEol) >> outFile_nc; - print("") >> outFile_nc; - print("\t" "const int groupId = -1;") >> outFile_nc; - print("\t" "return _" outName "(" paramListNoTypes ");") >> outFile_nc; - print("}") >> outFile_nc; - print("") >> outFile_nc; - - # Group version: - print("EXPORT(" retType ") " outName_group "(" paramList_group ") {" commentEol) >> outFile_nc; - print("") >> outFile_nc; - print("\t" "const int unitId = -1;") >> outFile_nc; - print("\t" "return _" outName "(" paramListNoTypes ");") >> outFile_nc; - print("}") >> outFile_nc; - } - } else { - print("Java-AIInterface: NOTE: native level: Commands: intentionally not wrapped: " fullName); - } - } - - print("// END: COMMAND_WRAPPERS") >> outFile_nh; - print("") >> outFile_nh; - print("#ifdef __cplusplus") >> outFile_nh; - print("} // extern \"C\"") >> outFile_nh; - print("#endif") >> outFile_nh; - print("") >> outFile_nh; - print("#endif // __CALLBACK_FUNCTION_POINTER_BRIDGE_H") >> outFile_nh; - print("") >> outFile_nh; - - print("") >> outFile_nc; - - close(outFile_nh); - close(outFile_nc); -} - - - - - - -# aggare te los command defines in order -/^[ \t]*COMMAND_.*$/ { - - sub(",", "", $4); - cmdsTopicNameValue[$2] = $4; -} - - - -# This function has to return true (1) if a doc comment (eg: /** foo bar */) -# can be deleted. -# If there is no special condition you want to apply, -# it should always return true (1), -# cause there are additional mechanism to prevent accidental deleting. -# see: commonDoc.awk -function canDeleteDocumentation() { - return isInsideCmdStruct != 1; -} - -################################################################################ -### BEGIN: parsing and saving the command structs - -# end of struct S*Command -/^}; \/\/\$ COMMAND_.*$/ { - - cmdsNumMembers[ind_cmdStructs] = ind_cmdMember; - cmdsTopicName[ind_cmdStructs] = $3; - metaInfo = $0; - sub(/^}; \/\/\$ COMMAND_[^ \t]+/, "", metaInfo); - metaInfo = trim(metaInfo); - cmdsMetaInfo[ind_cmdStructs] = metaInfo; - - if (doWrapp(ind_cmdStructs)) { - #printCommandJava(ind_cmdStructs); - } - - ind_cmdStructs++; - isInsideCmdStruct = 0; -} - - -# inside of struct S*Command -{ - if (isInsideCmdStruct == 1) { - size_tmpMembers = split($0, tmpMembers, ";"); - # cause there is an empty part behind the ';' - size_tmpMembers--; - for (i=1; i<=size_tmpMembers; i++) { - tmpMembers[i] = trim(tmpMembers[i]); - if (tmpMembers[i] == "" || match(tmpMembers[i], /^\/\//)) { - break; - } - # This would bork with more then 1000 members in a command, - # or more then 1000 commands - storeDocLines(cmdMbrsDocComments, ind_cmdStructs*1000 + ind_cmdMember); - saveMember(ind_cmdMember, tmpMembers[i]); - ind_cmdMember++; - } - } -} - -# beginning of struct S*Command -/^struct S.*Command( \{)?/ { - - isInsideCmdStruct = 1; - ind_cmdMember = 0; - commandName = $2; - sub(/^S/, "", commandName); - sub(/Command$/, "", commandName); - - isUnitCommand = match(commandName, /.*Unit$/); - - cmdsIsUnitCmd[ind_cmdStructs] = isUnitCommand; - cmdsName[ind_cmdStructs] = commandName; - storeDocLines(cmdsDocComment, ind_cmdStructs); -} - -# find COMMAND_TO_ID_ENGINE id -/COMMAND_TO_ID_ENGINE/ { - - cmdToIdEngine = $3; -} - -### END: parsing and saving the command structs -################################################################################ - - - -END { - # finalize things - - printNativeFP2F() -} diff --git a/AI/Interfaces/Java/data/InterfaceInfo.lua b/AI/Interfaces/Java/data/InterfaceInfo.lua deleted file mode 100644 index 13b586912a8..00000000000 --- a/AI/Interfaces/Java/data/InterfaceInfo.lua +++ /dev/null @@ -1,51 +0,0 @@ --- --- Info Definition Table format --- --- --- These keywords must be lowercase for LuaParser to read them. --- --- key: user defined or one of the AI_INTERFACE_PROPERTY_* defines in --- SAIInterfaceLibrary.h --- value: the value of the property --- desc: the description (could be used as a tooltip) --- --- --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- - -local infos = { - { - key = 'shortName', - value = 'Java', - desc = 'machine conform name.', - }, - { - key = 'version', - value = '0.1', - }, - { - key = 'name', - value = 'default Java AI Interface', - desc = 'human readable name.', - }, - { - key = 'description', - value = 'This interface is needed for Java AIs', - desc = 'tooltip.', - }, - { - key = 'url', - value = 'https://springrts.com/wiki/AIInterface:Java', - desc = 'URL with more detailed info about the AI', - }, - { - key = 'supportedLanguages', - value = 'Java (possily Groovy, JRuby, ...)', - }, - { - key = 'supportsLookup', - value = 'false', - }, -} - -return infos diff --git a/AI/Interfaces/Java/data/jvm.properties b/AI/Interfaces/Java/data/jvm.properties deleted file mode 100644 index a9642fc78ed..00000000000 --- a/AI/Interfaces/Java/data/jvm.properties +++ /dev/null @@ -1,86 +0,0 @@ -# These options are passed to the JVM instance in which all Java AIs run in. -# CAUTION: Please only change these settings if you know what you are doing!! -# For a list of possible options, please see: -# http://blogs.sun.com/watt/resource/jvm-options-list.html -# and: -# http://java.sun.com/javase/technologies/hotspot/vmoptions.jsp -# If you use do not use the SUN JVM, -# the supported options may differ from this list. -# Each line that does not start with '#' or ';' and contains -# other characters then whitespaces is taken as an option. -# -# NOTE: When specifying relative paths to files (eg. log files), -# be aware that these files will likely end up in springs writable data dir, -# though this is not guaranteed, which means you may have to search for them -# in the CWD of the JVM, which differs between platforms. -# -# NOTE: When specifying paths to files (eg. log files), -# you can make use of ${home-dir}, which will be replaced with something like: -# "{abs-path-to-spring-writable-data-dir}/AI/Interfaces/Java/0.1" -# - -# NOTE: only really useful for debugging -# false: crash and report error when a JVM option was specified, -# that is unknown to the used JVM -# default: true -;jvm.arguments.ignoreUnrecognized=false - -# NOTE: change this if you need fancy stuff only -# specify as hex value, eg. 0x00010004 is JNI_VERSION_1_4 -# see jni.h (look for JNI_VERSION_* defines) -# default: 0x00010004 -;jvm.jni.version=0x00010002 - - -# NOTE: this should not be used, as it is auto-generated -;jvm.option.java.class.path=... -# you may simply store .jar and .class files in the jlib/ folder -# of the Java AI Interface or your Java Skirmish AI. -# the value of this property is appended to the generated java.class.path - -# NOTE: you shall NOT specify the following, as they are auto-generated: -;jvm.option.java.library.path=... -# simply store .dll, .so or .dylib files in the lib/ folder -# of the Java AI Interface or your Java Skirmish AI. -# the value of this property is appended to the generated java.library.path - -# Specify which type of the JVM to use -# possible values: client, server -# default: 32bit: client -# 64bit: server -;jvm.type=server - -# NOTE: do not use these, as the interface will ignore them; -# see jvm.option.java.*.path options above -;jvm.option.x=-Djava.class.path=... -;jvm.option.x=-Djava.library.path=... - -# footprint (memory) related -jvm.option.x=-Xms64M -jvm.option.x=-Xmx512M -jvm.option.x=-Xss512K -jvm.option.x=-Xoss400K - - -# Misc -;jvm.option.x=-XX:+AlwaysRestoreFPU -;jvm.option.x=-Djava.util.logging.config.file=./logging.properties - -# example settings for debugging -# logging related (only recommended when debugging) -;jvm.option.x=-Xcheck:jni -;jvm.option.x=-verbose:jni -;jvm.option.x=-XX:+UnlockDiagnosticVMOptions -;jvm.option.x=-XX:+LogVMOutput -;jvm.option.x=-XX:LogFile=${home-dir}/log/jvm-log.xml - -;jvm.option.x=-Xdebug -;jvm.option.x=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=7777 -# disable JIT (required for debugging under the classical VM) -;jvm.option.x=-Djava.compiler=NONE -# disables old JDB -;jvm.option.x=-Xnoagent - -;jvm.option.x=-XX:ErrorFile=./hs_err_pid.log -;jvm.option.x=-XX:+CheckJNICalls - diff --git a/AI/Interfaces/Java/pom.xml b/AI/Interfaces/Java/pom.xml deleted file mode 100644 index bddb566ede4..00000000000 --- a/AI/Interfaces/Java/pom.xml +++ /dev/null @@ -1,49 +0,0 @@ - - 4.0.0 - - - - - - 0.1 - - - - com.springrts - common-spring - 1.0 - ../../../rts/build/maven/support/common/spring/pom.xml - - - com.springrts - ai-interface-java - ${my.version} - - jar - - Java AI Interface - Java Artificial Intelligence interface plugin for the Spring RTS engine - https://springrts.com/wiki/AIInterface:Java - 2008 - - - - - scm:git:git://github.com/spring/spring.git - scm:git:git@github.com:spring/spring.git - http://github.com/spring/spring/tree/master/AI/Interfaces/Java/ - - - diff --git a/AI/Interfaces/Java/src/main/java/com/springrts/ai/Util.java b/AI/Interfaces/Java/src/main/java/com/springrts/ai/Util.java deleted file mode 100644 index e3a45ebcc99..00000000000 --- a/AI/Interfaces/Java/src/main/java/com/springrts/ai/Util.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - Copyright (c) 2009 Robin Vobruba - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -*/ - -package com.springrts.ai; - - -import java.awt.Color; - -/** - * Contains utility functions used within all of the Java AI Interface code. - * - * @author hoijui.quaero@gmail.com - * @version 0.1 - */ -public final class Util { - - /** We need no instances of this class */ - private Util() {} - - public static short[] toShort3Array(final Color color) { - - short[] shortArr = new short[3]; - - shortArr[0] = (short) color.getRed(); - shortArr[1] = (short) color.getGreen(); - shortArr[2] = (short) color.getBlue(); - - return shortArr; - } - - public static Color toColor(final short[] shortArr) { - - Color color = new Color( - (int) shortArr[0], - (int) shortArr[1], - (int) shortArr[2] - ); - - return color; - } -} diff --git a/AI/Interfaces/Java/src/main/native/InterfaceDefines.h b/AI/Interfaces/Java/src/main/native/InterfaceDefines.h deleted file mode 100644 index ceb2ebcae59..00000000000 --- a/AI/Interfaces/Java/src/main/native/InterfaceDefines.h +++ /dev/null @@ -1,23 +0,0 @@ -/* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ - -#ifndef _INTERFACE_DEFINES_H -#define _INTERFACE_DEFINES_H - -#ifdef __cplusplus -extern "C" { -#endif - -#define JAVA_SKIRMISH_AI_PROPERTY_CLASS_NAME "className" - -#define MY_LOG_FILE "interface-log.txt" -#define JAVA_AI_INTERFACE_LIBRARY_FILE_NAME "AIInterface.jar" -#define NATIVE_LIBS_DIR "lib" -#define JRE_LOCATION_FILE "jre-location.txt" - -#include // for NULL - -#ifdef __cplusplus -} // extern "C" -#endif - -#endif // _INTERFACE_DEFINES_H diff --git a/AI/Interfaces/Java/src/main/native/InterfaceExport.c b/AI/Interfaces/Java/src/main/native/InterfaceExport.c deleted file mode 100644 index d68714c722e..00000000000 --- a/AI/Interfaces/Java/src/main/native/InterfaceExport.c +++ /dev/null @@ -1,157 +0,0 @@ -/* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ - -#include "InterfaceExport.h" - -#include "InterfaceDefines.h" -#include "JavaBridge.h" - -// generated at build time -#include "CallbackFunctionPointerBridge.h" - -#include "CUtils/Util.h" -#include "CUtils/SimpleLog.h" - -#include "ExternalAI/Interface/SAIInterfaceLibrary.h" -#include "ExternalAI/Interface/SAIInterfaceCallback.h" -#include "ExternalAI/Interface/SSkirmishAICallback.h" -#include "ExternalAI/Interface/SSkirmishAILibrary.h" - -#include // bool, true, false -#include // strlen(), strcat(), strcpy() -#include // malloc(), calloc(), free() - -static int interfaceId = -1; - -static const struct SAIInterfaceCallback* callback = NULL; -static struct SSkirmishAILibrary jvmSSkirmishAILibrary = {NULL, NULL, NULL, NULL}; - - -EXPORT(int) initStatic(int _interfaceId, const struct SAIInterfaceCallback* _callback) -{ - simpleLog_initcallback(_interfaceId, "Java Interface", _callback->Log_logsl, LOG_LEVEL_INFO); - - // initialize C part of the interface - interfaceId = _interfaceId; - callback = _callback; - - const char* const myShortName = callback->AIInterface_Info_getValueByKey(interfaceId, AI_INTERFACE_PROPERTY_SHORT_NAME); - const char* const myVersion = callback->AIInterface_Info_getValueByKey(interfaceId, AI_INTERFACE_PROPERTY_VERSION); - - if (myShortName == NULL || myVersion == NULL) { - simpleLog_logL(LOG_LEVEL_ERROR, "Couldn't fetch AI Name / Version \"%d\"", _interfaceId); - return -1; - } - - simpleLog_log("Initialized %s v%s AI Interface", myShortName, myVersion); - - // initialize Java part of the interface and the JVM - if (java_initStatic(interfaceId, callback)) { - simpleLog_logL(LOG_LEVEL_NOTICE, "Initialization successful."); - return 0; - } - - simpleLog_logL(LOG_LEVEL_ERROR, "Initialization failed."); - return -1; -} - -EXPORT(int) releaseStatic() -{ - // release Java part of the interface - if (java_releaseStatic() && java_unloadJNIEnv()) - return 0; - - return -1; -} - -EXPORT(enum LevelOfSupport) getLevelOfSupportFor(const char* engineVersion, int engineAIInterfaceGeneratedVersion) -{ - return LOS_Unknown; -} - - -// skirmish AI methods -enum LevelOfSupport CALLING_CONV proxy_skirmishAI_getLevelOfSupportFor( - const char* aiShortName, - const char* aiVersion, - const char* engineVersionString, - int engineVersionNumber, - const char* aiInterfaceShortName, - const char* aiInterfaceVersion -) { - return LOS_Unknown; -} - - -int CALLING_CONV proxy_skirmishAI_init(int skirmishAIId, const struct SSkirmishAICallback* aiCallback) -{ - int ret = -1; - - const char* const shortName = aiCallback->SkirmishAI_Info_getValueByKey(skirmishAIId, SKIRMISH_AI_PROPERTY_SHORT_NAME); - const char* const version = aiCallback->SkirmishAI_Info_getValueByKey(skirmishAIId, SKIRMISH_AI_PROPERTY_VERSION); - const char* const className = aiCallback->SkirmishAI_Info_getValueByKey(skirmishAIId, JAVA_SKIRMISH_AI_PROPERTY_CLASS_NAME); - - if (className != NULL) - ret = java_initSkirmishAIClass(shortName, version, className, skirmishAIId) ? 0 : 1; - - if (ret == 0) { - // init OK - funcPntBrdg_addCallback(skirmishAIId, aiCallback); - ret = java_skirmishAI_init(skirmishAIId, aiCallback); - } - - return ret; -} - -int CALLING_CONV proxy_skirmishAI_release(int skirmishAIId) -{ - // does nothing - const int ret = java_skirmishAI_release(skirmishAIId); - funcPntBrdg_removeCallback(skirmishAIId); - return ret; -} - -int CALLING_CONV proxy_skirmishAI_handleEvent(int skirmishAIId, int topicId, const void* data) -{ - return java_skirmishAI_handleEvent(skirmishAIId, topicId, data); -} - - -EXPORT(const struct SSkirmishAILibrary*) loadSkirmishAILibrary( - const char* const shortName, - const char* const version -) { - // all Java AI's run inside a single JVM proxied by this lib - if (jvmSSkirmishAILibrary.init == NULL) { - jvmSSkirmishAILibrary.getLevelOfSupportFor = &proxy_skirmishAI_getLevelOfSupportFor; -/* - jvmSSkirmishAILibrary.getInfo = proxy_skirmishAI_getInfo; - jvmSSkirmishAILibrary.getOptions = proxy_skirmishAI_getOptions; -*/ - jvmSSkirmishAILibrary.init = &proxy_skirmishAI_init; - jvmSSkirmishAILibrary.release = &proxy_skirmishAI_release; - jvmSSkirmishAILibrary.handleEvent = &proxy_skirmishAI_handleEvent; - } - - return &jvmSSkirmishAILibrary; -} - -EXPORT(int) unloadSkirmishAILibrary( - const char* const shortName, - const char* const version -) { - const char* const className = callback->SkirmishAI_Info_getValueByKey(interfaceId, shortName, version, JAVA_SKIRMISH_AI_PROPERTY_CLASS_NAME); - - if (java_releaseSkirmishAIClass(className)) - return 0; - - return -1; -} - -EXPORT(int) unloadAllSkirmishAILibraries() -{ - if (java_releaseAllSkirmishAIClasses()) - return 0; - - return -1; -} - diff --git a/AI/Interfaces/Java/src/main/native/InterfaceExport.h b/AI/Interfaces/Java/src/main/native/InterfaceExport.h deleted file mode 100644 index 20920ccb563..00000000000 --- a/AI/Interfaces/Java/src/main/native/InterfaceExport.h +++ /dev/null @@ -1,61 +0,0 @@ -/* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ - -#ifndef _INTERFACE_EXPORT_H -#define _INTERFACE_EXPORT_H - -// check if the correct defines are set by the build system -#if !defined BUILDING_AI_INTERFACE -# error BUILDING_AI_INTERFACE should be defined when building AI Interfaces -#endif -#if !defined BUILDING_AI -# error BUILDING_AI should be defined when building AI Interfaces -#endif -#if defined BUILDING_SKIRMISH_AI -# error BUILDING_SKIRMISH_AI should not be defined when building AI Interfaces -#endif -#if defined SYNCIFY -# error SYNCIFY should not be defined when building AI Interfaces -#endif - - -#include "ExternalAI/Interface/aidefines.h" - -#ifdef __cplusplus -extern "C" { -#endif - -//#include "ExternalAI/Interface/ELevelOfSupport.h" - -struct SSkirmishAILibrary; -struct SAIInterfaceCallback; - -// for a list of the functions that have to be exported, -// see struct SAIInterfaceLibrary in: -// "rts/ExternalAI/Interface/SAIInterfaceLibrary.h" - - -// static AI interface library functions - -EXPORT(int) initStatic(int interfaceId, - const struct SAIInterfaceCallback* callback); -EXPORT(int) releaseStatic(); -//EXPORT(enum LevelOfSupport) getLevelOfSupportFor( -// const char* engineVersion, int engineAIInterfaceGeneratedVersion); - - -// skirmish AI related methods - -EXPORT(const struct SSkirmishAILibrary*) loadSkirmishAILibrary( - const char* const shortName, - const char* const version); -EXPORT(int) unloadSkirmishAILibrary( - const char* const shortName, - const char* const version); -EXPORT(int) unloadAllSkirmishAILibraries(); - - -#ifdef __cplusplus -} // extern "C" -#endif - -#endif // _INTERFACE_EXPORT_H diff --git a/AI/Interfaces/Java/src/main/native/JavaBridge.c b/AI/Interfaces/Java/src/main/native/JavaBridge.c deleted file mode 100644 index 1827f63c806..00000000000 --- a/AI/Interfaces/Java/src/main/native/JavaBridge.c +++ /dev/null @@ -1,1219 +0,0 @@ -/* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ - -#include "JavaBridge.h" - -#include "InterfaceDefines.h" -#include "JvmLocater.h" -#include "JniUtil.h" -#include "EventsJNIBridge.h" -#include "CUtils/Util.h" -#include "CUtils/SimpleLog.h" -#include "CUtils/SharedLibrary.h" - -#include "ExternalAI/Interface/aidefines.h" -#include "ExternalAI/Interface/SAIInterfaceLibrary.h" -#include "ExternalAI/Interface/SAIInterfaceCallback.h" -#include "ExternalAI/Interface/SSkirmishAILibrary.h" -#include "ExternalAI/Interface/SSkirmishAICallback.h" -#include "System/SafeCStrings.h" -#include "lib/streflop/streflopC.h" - -#include - -#include // strlen(), strcat(), strcpy() -#include // malloc(), calloc(), free() -#include - -struct Properties { - size_t size; - const char** keys; - const char** values; -}; - -static int interfaceId = -1; - -static const struct SAIInterfaceCallback* callback = NULL; -static struct Properties jvmCfgProps = {0, NULL, NULL}; - -static size_t numSkirmishAIs = 0; -// static const size_t maxSkirmishAIs = 255; // MAX_AIS -#define maxSkirmishAIs 255 - -static size_t skirmishAIId_skirmishAiImpl[maxSkirmishAIs] = {999999}; - -static char* jAIClassNames[maxSkirmishAIs] = {NULL}; - -static jobject jAIInstances[maxSkirmishAIs] = {NULL}; -static jobject jAIClassLoaders[maxSkirmishAIs] = {NULL}; -static jobject jAICallBacks[maxSkirmishAIs] = {NULL}; - -// vars used to integrate the JVM -// it is loaded at runtime, not at loadtime -static sharedLib_t jvmSharedLib = NULL; - -typedef jint (JNICALL JNI_GetDefaultJavaVMInitArgs_t)(void* vmArgs); -typedef jint (JNICALL JNI_CreateJavaVM_t)(JavaVM** vm, void** jniEnv, void* vmArgs); -typedef jint (JNICALL JNI_GetCreatedJavaVMs_t)(JavaVM** vms, jsize vms_sizeMax, jsize* vms_size); - -static JNI_GetDefaultJavaVMInitArgs_t* JNI_GetDefaultJavaVMInitArgs_f; -static JNI_CreateJavaVM_t* JNI_CreateJavaVM_f; -static JNI_GetCreatedJavaVMs_t* JNI_GetCreatedJavaVMs_f; - - -// ### JNI global vars ### - -/// Java VM instance reference -static JavaVM* g_jvm = NULL; - -/// AI Callback class -static jclass g_cls_aiCallback = NULL; -/// AI Callback Constructor: AICallback(int skirmishAIId) -static jmethodID g_m_aiCallback_ctor_I = NULL; - -/// Basic AI interface. -static jclass g_cls_ai_int = NULL; - - - -// ### General helper functions following ### - -/// Sets the FPU state to how spring likes it -static inline void java_establishSpringEnv() { - // only detach in java_unloadJNIEnv - // (*g_jvm)->DetachCurrentThread(g_jvm); - streflop_init_Simple(); -} - -/// The JVM sets the environment it wants automatically, so this is a no-op -static inline void java_establishJavaEnv() {} - - -static inline size_t minSize(size_t size1, size_t size2) { - return (size1 < size2) ? size1 : size2; -} - -static const char* java_getValueByKey(const struct Properties* props, const char* key) { - return util_map_getValueByKey(props->size, props->keys, props->values, key); -} - - -// /** -// * Called when the JVM loads this native library. -// */ -// JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { -// return JNI_VERSION_1_6; -// } -// -// /** -// * Called when the JVM unloads this native library. -// */ -// JNIEXPORT void JNICALL JNI_OnUnload(JavaVM* vm, void* reserved) {} - - - - -// ### JNI helper functions following ### - -/** - * Creates the AI Interface global Java class path. - * - * It will consist of the following: - * {spring-data-dir}/{AI_INTERFACES_DATA_DIR}/Java/{version}/AIInterface.jar - * {spring-data-dir}/{AI_INTERFACES_DATA_DIR}/Java/{version}/(j)?config/ - * {spring-data-dir}/{AI_INTERFACES_DATA_DIR}/Java/{version}/(j)?config/[*].jar - * {spring-data-dir}/{AI_INTERFACES_DATA_DIR}/Java/{version}/(j)?resources/ - * {spring-data-dir}/{AI_INTERFACES_DATA_DIR}/Java/{version}/(j)?resources/[*].jar - * {spring-data-dir}/{AI_INTERFACES_DATA_DIR}/Java/{version}/(j)?script/ - * {spring-data-dir}/{AI_INTERFACES_DATA_DIR}/Java/{version}/(j)?script/[*].jar - * {spring-data-dir}/{AI_INTERFACES_DATA_DIR}/Java/{version}/jlib/ - * {spring-data-dir}/{AI_INTERFACES_DATA_DIR}/Java/{version}/jlib/[*].jar - * TODO: {spring-data-dir}/{AI_INTERFACES_DATA_DIR}/Java/common/jlib/ - * TODO: {spring-data-dir}/{AI_INTERFACES_DATA_DIR}/Java/common/jlib/[*].jar - */ -static bool java_createClassPath(char* classPathStr, const size_t classPathStr_sizeMax) -{ - // the dirs and .jar files in the following array - // will be concatenated with intermediate path separators - // to form the classPathStr - static const size_t classPath_sizeMax = 128; - char** classPath = (char**) calloc(classPath_sizeMax, sizeof(char*)); - size_t classPath_size = 0; - - // the Java AI Interfaces java library file path (.../AIInterface.jar) - // We need to search for this jar, instead of looking only where - // the AIInterface.so/InterfaceInfo.lua is, because on some systems - // (eg. Debian), the .so is in /usr/lib, and the .jar's are in /usr/shared. - char mainJarPath[2048]; - const bool located = callback->DataDirs_locatePath(interfaceId, - mainJarPath, sizeof(mainJarPath), - JAVA_AI_INTERFACE_LIBRARY_FILE_NAME, - false, false, false, false); - - if (!located) { - simpleLog_logL(LOG_LEVEL_ERROR, "Couldn't find %s", JAVA_AI_INTERFACE_LIBRARY_FILE_NAME); - return false; - } - - classPath[classPath_size++] = util_allocStrCpy(mainJarPath); - - if (!util_getParentDir(mainJarPath)) { - simpleLog_logL(LOG_LEVEL_ERROR, "Retrieving the parent dir of the path to AIInterface.jar (%s) failed.", mainJarPath); - return false; - } - - char* jarsDataDir = mainJarPath; - - // the directories in the following list will be searched for .jar files - // which will then be added to the classPathStr, plus the dirs will be added - // to the classPathStr directly, so you can keep .class files in there - static const size_t jarDirs_sizeMax = 128; - char** jarDirs = (char**) calloc(jarDirs_sizeMax, sizeof(char*)); - size_t jarDirs_size = 0; - - // add to classpath: - // {spring-data-dir}/Interfaces/Java/0.1/${x}/ - jarDirs[jarDirs_size++] = util_allocStrCatFSPath(2, jarsDataDir, "jconfig"); - jarDirs[jarDirs_size++] = util_allocStrCatFSPath(2, jarsDataDir, "config"); - jarDirs[jarDirs_size++] = util_allocStrCatFSPath(2, jarsDataDir, "jresources"); - jarDirs[jarDirs_size++] = util_allocStrCatFSPath(2, jarsDataDir, "resources"); - jarDirs[jarDirs_size++] = util_allocStrCatFSPath(2, jarsDataDir, "jscript"); - jarDirs[jarDirs_size++] = util_allocStrCatFSPath(2, jarsDataDir, "script"); - jarDirs[jarDirs_size++] = util_allocStrCatFSPath(2, jarsDataDir, "jlib"); - // "lib" is for native libs only - - // add the jar dirs (for .class files) and all contained .jars recursively - size_t jd, jf; - for (jd = 0; (jd < jarDirs_size) && (classPath_size < classPath_sizeMax); ++jd) { - if (util_fileExists(jarDirs[jd])) { - // add the dir directly - // For this to work properly with URLClassPathHandler, - // we have to ensure there is a '/' at the end, - // for the class-path-part to be recognized as a directory. - classPath[classPath_size++] = util_allocStrCat(2, jarDirs[jd], "/"); - - // add the contained jars recursively - static const size_t jarFiles_sizeMax = 128; - char** jarFiles = (char**) calloc(jarFiles_sizeMax, sizeof(char*)); - const size_t jarFiles_size = util_listFiles(jarDirs[jd], ".jar", jarFiles, true, jarFiles_sizeMax); - - for (jf = 0; (jf < jarFiles_size) && (classPath_size < classPath_sizeMax); ++jf) { - classPath[classPath_size++] = util_allocStrCatFSPath(2, jarDirs[jd], jarFiles[jf]); - FREE(jarFiles[jf]); - } - - FREE(jarFiles); - } - - FREE(jarDirs[jd]); - } - - FREE(jarDirs); - - - // concat the classpath entries - classPathStr[0] = '\0'; - if (classPath[0] != NULL) { - STRCAT_T(classPathStr, classPathStr_sizeMax, classPath[0]); - FREE(classPath[0]); - } - - size_t cp; - - for (cp = 1; cp < classPath_size; ++cp) { - if (classPath[cp] == NULL) - continue; - - STRCAT_T(classPathStr, classPathStr_sizeMax, ENTRY_DELIM); - STRCAT_T(classPathStr, classPathStr_sizeMax, classPath[cp]); - FREE(classPath[cp]); - } - - FREE(classPath); - return true; -} - -/** - * Creates a Skirmish AI local Java class path. - * - * It will consist of the following: - * {spring-data-dir}/{SKIRMISH_AI_DATA_DIR}/{ai-name}/{ai-version}/SkirmishAI.jar - * {spring-data-dir}/{SKIRMISH_AI_DATA_DIR}/{ai-name}/{ai-version}/(j)?config/ - * {spring-data-dir}/{SKIRMISH_AI_DATA_DIR}/{ai-name}/{ai-version}/(j)?config/[*].jar - * {spring-data-dir}/{SKIRMISH_AI_DATA_DIR}/{ai-name}/{ai-version}/(j)?resources/ - * {spring-data-dir}/{SKIRMISH_AI_DATA_DIR}/{ai-name}/{ai-version}/(j)?resources/[*].jar - * {spring-data-dir}/{SKIRMISH_AI_DATA_DIR}/{ai-name}/{ai-version}/(j)?script/ - * {spring-data-dir}/{SKIRMISH_AI_DATA_DIR}/{ai-name}/{ai-version}/(j)?script/[*].jar - * {spring-data-dir}/{SKIRMISH_AI_DATA_DIR}/{ai-name}/{ai-version}/jlib/ - * {spring-data-dir}/{SKIRMISH_AI_DATA_DIR}/{ai-name}/{ai-version}/jlib/[*].jar - * {spring-data-dir}/{SKIRMISH_AI_DATA_DIR}/{ai-name}/common/(j)?config/ - * {spring-data-dir}/{SKIRMISH_AI_DATA_DIR}/{ai-name}/common/(j)?config/[*].jar - * {spring-data-dir}/{SKIRMISH_AI_DATA_DIR}/{ai-name}/common/(j)?resources/ - * {spring-data-dir}/{SKIRMISH_AI_DATA_DIR}/{ai-name}/common/(j)?resources/[*].jar - * {spring-data-dir}/{SKIRMISH_AI_DATA_DIR}/{ai-name}/common/(j)?script/ - * {spring-data-dir}/{SKIRMISH_AI_DATA_DIR}/{ai-name}/common/(j)?script/[*].jar - * {spring-data-dir}/{SKIRMISH_AI_DATA_DIR}/{ai-name}/common/jlib/ - * {spring-data-dir}/{SKIRMISH_AI_DATA_DIR}/{ai-name}/common/jlib/[*].jar - */ -static size_t java_createAIClassPath( - const char* shortName, - const char* version, - char** classPathParts, - const size_t classPathParts_sizeMax -) { - size_t classPathParts_size = 0; - - // the .jar files in the following list will be added to the classpath - const size_t jarFiles_sizeMax = classPathParts_sizeMax; - char** jarFiles = (char**) calloc(jarFiles_sizeMax, sizeof(char*)); - size_t jarFiles_size = 0; - - const char* const skirmDD = - callback->SkirmishAI_Info_getValueByKey(interfaceId, - shortName, version, - SKIRMISH_AI_PROPERTY_DATA_DIR); - if (skirmDD == NULL) { - simpleLog_logL(LOG_LEVEL_ERROR, - "Retrieving the data-dir of Skirmish AI %s-%s failed.", - shortName, version); - } - // {spring-data-dir}/{SKIRMISH_AI_DATA_DIR}/{ai-name}/{ai-version}/SkirmishAI.jar - jarFiles[jarFiles_size++] = util_allocStrCatFSPath(2, - skirmDD, "SkirmishAI.jar"); - - // the directories in the following list will be searched for .jar files - // which then will be added to the classpath, plus they will be added - // to the classpath directly, so you can keep .class files in there - const size_t jarDirs_sizeMax = classPathParts_sizeMax; - char** jarDirs = (char**) calloc(jarDirs_sizeMax, sizeof(char*)); - size_t jarDirs_size = 0; - - // add to classpath ... - - // {spring-data-dir}/Skirmish/MyJavaAI/0.1/SkirmishAI/ - // this can be useful for AI devs while testing, - // if they do not want to put everything into a jar all the time - jarDirs[jarDirs_size++] = util_allocStrCatFSPath(2, skirmDD, "SkirmishAI"); - - // add to classpath: - // {spring-data-dir}/Skirmish/MyJavaAI/0.1/${x}/ - jarDirs[jarDirs_size++] = util_allocStrCatFSPath(2, skirmDD, "jconfig"); - jarDirs[jarDirs_size++] = util_allocStrCatFSPath(2, skirmDD, "config"); - jarDirs[jarDirs_size++] = util_allocStrCatFSPath(2, skirmDD, "jresources"); - jarDirs[jarDirs_size++] = util_allocStrCatFSPath(2, skirmDD, "resources"); - jarDirs[jarDirs_size++] = util_allocStrCatFSPath(2, skirmDD, "jscript"); - jarDirs[jarDirs_size++] = util_allocStrCatFSPath(2, skirmDD, "script"); - jarDirs[jarDirs_size++] = util_allocStrCatFSPath(2, skirmDD, "jlib"); - // "lib" is for native libs only - - // add the dir common for all versions of the Skirmish AI, - // if it is specified and exists - const char* const skirmDDCommon = - callback->SkirmishAI_Info_getValueByKey(interfaceId, - shortName, version, - SKIRMISH_AI_PROPERTY_DATA_DIR_COMMON); - - if (skirmDDCommon != NULL) { - // add to classpath: - // {spring-data-dir}/Skirmish/MyJavaAI/common/${x}/ - jarDirs[jarDirs_size++] = util_allocStrCatFSPath(2, skirmDDCommon, "jconfig"); - jarDirs[jarDirs_size++] = util_allocStrCatFSPath(2, skirmDDCommon, "config"); - jarDirs[jarDirs_size++] = util_allocStrCatFSPath(2, skirmDDCommon, "jresources"); - jarDirs[jarDirs_size++] = util_allocStrCatFSPath(2, skirmDDCommon, "resources"); - jarDirs[jarDirs_size++] = util_allocStrCatFSPath(2, skirmDDCommon, "jscript"); - jarDirs[jarDirs_size++] = util_allocStrCatFSPath(2, skirmDDCommon, "script"); - jarDirs[jarDirs_size++] = util_allocStrCatFSPath(2, skirmDDCommon, "jlib"); - // "lib" is for native libs only - } - - // add the directly specified .jar files - size_t jf; - for (jf = 0; (jf < jarFiles_size) && (classPathParts_size < classPathParts_sizeMax); ++jf) { - classPathParts[classPathParts_size++] = util_allocStrCpy(jarFiles[jf]); - FREE(jarFiles[jf]); - } - - // add the dirs and the contained .jar files - size_t jd, sjf; - for (jd = 0; (jd < jarDirs_size) && (classPathParts_size < classPathParts_sizeMax); ++jd) { - if (jarDirs[jd] != NULL && util_fileExists(jarDirs[jd])) { - // add the jar dir (for .class files) - // For this to work properly with URLClassPathHandler, - // we have to ensure there is a '/' at the end, - // for the class-path-part to be recognized as a directory. - classPathParts[classPathParts_size++] = util_allocStrCat(2, jarDirs[jd], "/"); - - // add the jars in the dir - const size_t subJarFiles_sizeMax = classPathParts_sizeMax - classPathParts_size; - char** subJarFiles = (char**) calloc(subJarFiles_sizeMax, sizeof(char*)); - const size_t subJarFiles_size = util_listFiles(jarDirs[jd], ".jar", subJarFiles, true, subJarFiles_sizeMax); - - for (sjf = 0; (sjf < subJarFiles_size) && (classPathParts_size < classPathParts_sizeMax); ++sjf) { - // .../[*].jar - classPathParts[classPathParts_size++] = util_allocStrCatFSPath(2, jarDirs[jd], subJarFiles[sjf]); - FREE(subJarFiles[sjf]); - } - - FREE(subJarFiles); - } - - FREE(jarDirs[jd]); - } - - FREE(jarDirs); - FREE(jarFiles); - - return classPathParts_size; -} - -static jobject java_createAIClassLoader(JNIEnv* env, const char* shortName, const char* version) -{ - static const size_t classPathParts_sizeMax = 512; - char** classPathParts = (char**) calloc(classPathParts_sizeMax, sizeof(char*)); - const size_t classPathParts_size = java_createAIClassPath(shortName, version, classPathParts, classPathParts_sizeMax); - - jobject o_jClsLoader = NULL; - jobjectArray o_cppURLs = jniUtil_createURLArray(env, classPathParts_size); - - if (o_cppURLs != NULL) { -#ifdef _WIN32 - static const char* FILE_URL_PREFIX = "file:///"; -#else // _WIN32 - static const char* FILE_URL_PREFIX = "file://"; -#endif // _WIN32 - size_t cpp; - for (cpp = 0; cpp < classPathParts_size; ++cpp) { - #ifdef _WIN32 - // we can not use windows path separators in file URLs - util_strReplaceChar(classPathParts[cpp], '\\', '/'); - #endif - - char* str_fileUrl = util_allocStrCat(2, FILE_URL_PREFIX, classPathParts[cpp]); - // TODO: check/test if this is allowed/ok - FREE(classPathParts[cpp]); - simpleLog_logL(LOG_LEVEL_INFO, - "Skirmish AI %s %s class-path part %i: \"%s\"", - shortName, version, cpp, str_fileUrl); - jobject jurl_fileUrl = jniUtil_createURLObject(env, str_fileUrl); - FREE(str_fileUrl); - if (jurl_fileUrl == NULL) { - simpleLog_logL(LOG_LEVEL_ERROR, - "Skirmish AI %s %s class-path part %i (\"%s\"): failed to create a URL", - shortName, version, cpp, str_fileUrl); - o_cppURLs = NULL; - break; - } - const bool inserted = jniUtil_insertURLIntoArray(env, o_cppURLs, cpp, jurl_fileUrl); - if (!inserted) { - simpleLog_logL(LOG_LEVEL_ERROR, - "Skirmish AI %s %s class-path part %i (\"%s\"): failed to insert", - shortName, version, cpp, str_fileUrl); - o_cppURLs = NULL; - break; - } - } - } - - if (o_cppURLs != NULL) { - if ((o_jClsLoader = jniUtil_createURLClassLoader(env, o_cppURLs)) != NULL) - o_jClsLoader = jniUtil_makeGlobalRef(env, o_jClsLoader, "Skirmish AI class-loader"); - } - - FREE(classPathParts); - return o_jClsLoader; -} - -/** - * Load the interfaces JVM properties file. - */ -static bool java_readJvmCfgFile(struct Properties* props) -{ - bool read = false; - - const size_t props_sizeMax = 256; - - props->size = 0; - props->keys = (const char**) calloc(props_sizeMax, sizeof(char*)); - props->values = (const char**) calloc(props_sizeMax, sizeof(char*)); - - // ### read JVM options config file ### - char jvmPropFile[2048]; - bool located = callback->DataDirs_locatePath(interfaceId, jvmPropFile, sizeof(jvmPropFile), JVM_PROPERTIES_FILE, false, false, false, false); - - // if the version specific file does not exist, - // try to get the common one - if (!located) - located = callback->DataDirs_locatePath(interfaceId, jvmPropFile, sizeof(jvmPropFile), JVM_PROPERTIES_FILE, false, false, false, true); - - if (located) { - props->size = util_parsePropertiesFile(jvmPropFile, props->keys, props->values, props_sizeMax); - read = true; - simpleLog_logL(LOG_LEVEL_INFO, "JVM: arguments loaded from: %s", jvmPropFile); - } else { - props->size = 0; - read = false; - simpleLog_logL(LOG_LEVEL_INFO, "JVM: arguments NOT loaded"); - } - - return read; -} - -/** - * Creates the Java library path. - * -> where native shared libraries are searched - * - * It will consist of the following: - * {spring-data-dir}/{AI_INTERFACES_DATA_DIR}/Java/{version}/ - * {spring-data-dir}/{AI_INTERFACES_DATA_DIR}/Java/{version}/lib/ - * {spring-data-dir}/{AI_INTERFACES_DATA_DIR}/Java/common/ - * {spring-data-dir}/{AI_INTERFACES_DATA_DIR}/Java/common/lib/ - */ -static bool java_createNativeLibsPath(char* libraryPath, const size_t libraryPath_sizeMax) -{ - // {spring-data-dir}/{AI_INTERFACES_DATA_DIR}/Java/{version}/ - const char* const dd_r = callback->AIInterface_Info_getValueByKey(interfaceId, AI_INTERFACE_PROPERTY_DATA_DIR); - - if (dd_r == NULL) { - simpleLog_logL(LOG_LEVEL_ERROR, "Unable to find read-only data-dir."); - return false; - } - - STRCPY_T(libraryPath, libraryPath_sizeMax, dd_r); - - // {spring-data-dir}/{AI_INTERFACES_DATA_DIR}/Java/{version}/lib/ - char dd_lib_r[2048]; - const bool located = callback->DataDirs_locatePath(interfaceId, dd_lib_r, sizeof(dd_lib_r), NATIVE_LIBS_DIR, false, false, true, false); - - if (!located) { - simpleLog_logL(LOG_LEVEL_NOTICE, "Unable to find read-only native libs data-dir (optional): %s", NATIVE_LIBS_DIR); - } else { - STRCAT_T(libraryPath, libraryPath_sizeMax, ENTRY_DELIM); - STRCAT_T(libraryPath, libraryPath_sizeMax, dd_lib_r); - } - - - // {spring-data-dir}/{AI_INTERFACES_DATA_DIR}/Java/common/ - const char* const dd_r_common = callback->AIInterface_Info_getValueByKey(interfaceId, AI_INTERFACE_PROPERTY_DATA_DIR_COMMON); - - if (dd_r_common == NULL || !util_fileExists(dd_r_common)) { - simpleLog_logL(LOG_LEVEL_NOTICE, "Unable to find common read-only data-dir (optional)."); - } else { - STRCAT_T(libraryPath, libraryPath_sizeMax, ENTRY_DELIM); - STRCAT_T(libraryPath, libraryPath_sizeMax, dd_r_common); - } - - - // {spring-data-dir}/{AI_INTERFACES_DATA_DIR}/Java/common/lib/ - if (dd_r_common != NULL) { - char dd_lib_r_common[2048]; - const bool located = callback->DataDirs_locatePath(interfaceId, dd_lib_r_common, sizeof(dd_lib_r_common), NATIVE_LIBS_DIR, false, false, true, true); - - if (!located || !util_fileExists(dd_lib_r_common)) { - simpleLog_logL(LOG_LEVEL_NOTICE, "Unable to find common read-only native libs data-dir (optional)."); - } else { - STRCAT_T(libraryPath, libraryPath_sizeMax, ENTRY_DELIM); - STRCAT_T(libraryPath, libraryPath_sizeMax, dd_lib_r_common); - } - } - - return true; -} - - -static bool java_createJavaVMInitArgs(struct JavaVMInitArgs* vm_args, const struct Properties* jvmProps) -{ - // jint jniVersion = JNI_VERSION_1_1; - // jint jniVersion = JNI_VERSION_1_2; - jint jniVersion = JNI_VERSION_1_4; - // jint jniVersion = JNI_VERSION_1_6; - - char classPath [8 * 1024] = {0}; - char classPathOpt[8 * 1024] = {0}; - char libraryPath [4 * 1024] = {0}; - char libraryPathOpt[4 * 1024] = {0}; - - bool ignoreUnrecognized = true; - - if (jvmProps != NULL) { - const char* jniVersionFromCfg = java_getValueByKey(jvmProps, "jvm.jni.version"); - const char* ignoreUnrecognizedFromCfg = java_getValueByKey(jvmProps, "jvm.arguments.ignoreUnrecognized"); - - // ### evaluate JNI version to use ### - if (jniVersionFromCfg != NULL) { - const unsigned long int jniVersion_tmp = strtoul(jniVersionFromCfg, NULL, 16); - - if (jniVersion_tmp != 0 /*&& jniVersion_tmp != ULONG_MAX*/) - jniVersion = (jint) jniVersion_tmp; - } - - // ### check if unrecognized JVM options should be ignored ### - // if false, the JVM creation will fail if an - // unknown or invalid option was specified - if (ignoreUnrecognizedFromCfg != NULL && !util_strToBool(ignoreUnrecognizedFromCfg)) - ignoreUnrecognized = false; - } - - simpleLog_logL(LOG_LEVEL_INFO, "JVM: JNI version: %#x", jniVersion); - vm_args->version = jniVersion; - - - if (ignoreUnrecognized) { - simpleLog_logL(LOG_LEVEL_INFO, "JVM: ignoring unrecognized options"); - vm_args->ignoreUnrecognized = JNI_TRUE; - } else { - simpleLog_logL(LOG_LEVEL_INFO, "JVM: NOT ignoring unrecognized options"); - vm_args->ignoreUnrecognized = JNI_FALSE; - } - - - // ### create the Java class-path option ### - // autogenerate the class path - if (!java_createClassPath(classPath, sizeof(classPath))) { - simpleLog_logL(LOG_LEVEL_ERROR, "Failed creating Java class-path."); - return false; - } - if (jvmProps != NULL) { - // ..., and append the part from the jvm options properties file, - // if it is specified there - const char* clsPathFromCfg = java_getValueByKey(jvmProps, "jvm.option.java.class.path"); - - if (clsPathFromCfg != NULL) { - STRCAT_T(classPath, sizeof(classPath), ENTRY_DELIM); - STRCAT_T(classPath, sizeof(classPath), clsPathFromCfg); - } - } - - - // create the java.class.path option - STRCPY_T(classPathOpt, sizeof(classPathOpt), "-Djava.class.path="); - STRCAT_T(classPathOpt, sizeof(classPathOpt), classPath); - - - // ### create the Java library-path option ### - // autogenerate the java library path - if (!java_createNativeLibsPath(libraryPath, sizeof(libraryPath))) { - simpleLog_logL(LOG_LEVEL_ERROR, "Failed creating Java library-path."); - return false; - } - if (jvmProps != NULL) { - // ..., and append the part from the jvm options properties file, - // if it is specified there - const char* libPathFromCfg = java_getValueByKey(jvmProps, "jvm.option.java.library.path"); - - if (libPathFromCfg != NULL) { - STRCAT_T(libraryPath, sizeof(libraryPath), ENTRY_DELIM); - STRCAT_T(libraryPath, sizeof(libraryPath), libPathFromCfg); - } - } - - // create the java.library.path option ... - // autogenerate it, and append the part from the jvm options file, - // if it is specified there - STRCPY_T(libraryPathOpt, sizeof(libraryPathOpt), "-Djava.library.path="); - STRCAT_T(libraryPathOpt, sizeof(libraryPathOpt), libraryPath); - - - // ### create and set all JVM options ### - const char* strOptions[64]; - size_t numOpts = 0; - - strOptions[numOpts++] = classPathOpt; - strOptions[numOpts++] = libraryPathOpt; - - static const char* const JCPVAL = "-Djava.class.path="; - static const char* const JLPVAL = "-Djava.library.path="; - const size_t JCPVAL_size = strlen(JCPVAL); - const size_t JLPVAL_size = strlen(JCPVAL); - - if (jvmProps != NULL) { - // ### add string options from the JVM config file with property name "jvm.option.x" ### - int i; - for (i = 0; i < jvmProps->size; ++i) { - if (strcmp(jvmProps->keys[i], "jvm.option.x") != 0) - continue; - - const char* const val = jvmProps->values[i]; - const size_t val_size = strlen(val); - // ignore "-Djava.class.path=..." - // and "-Djava.library.path=..." options - if (strncmp(val, JCPVAL, minSize(val_size, JCPVAL_size)) != 0 && - strncmp(val, JLPVAL, minSize(val_size, JLPVAL_size)) != 0) { - strOptions[numOpts++] = val; - } - } - } else { - // ### ... or set default ones, if the JVM config file was not found ### - simpleLog_logL(LOG_LEVEL_WARNING, "JVM: properties file ("JVM_PROPERTIES_FILE") not found; using default options."); - - strOptions[numOpts++] = "-Xms64M"; - strOptions[numOpts++] = "-Xmx512M"; - strOptions[numOpts++] = "-Xss512K"; - strOptions[numOpts++] = "-Xoss400K"; - - #if defined DEBUG - strOptions[numOpts++] = "-Xcheck:jni"; - strOptions[numOpts++] = "-verbose:jni"; - strOptions[numOpts++] = "-XX:+UnlockDiagnosticVMOptions"; - strOptions[numOpts++] = "-XX:+LogVMOutput"; - - strOptions[numOpts++] = "-Xdebug"; - strOptions[numOpts++] = "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=7777"; - // disable JIT (required for debugging under the classical VM) - strOptions[numOpts++] = "-Djava.compiler=NONE"; - // disable old JDB - strOptions[numOpts++] = "-Xnoagent"; - #endif // defined DEBUG - } - - vm_args->options = (struct JavaVMOption*) calloc(numOpts, sizeof(struct JavaVMOption)); - vm_args->nOptions = 0; - - // fill strOptions into the JVM options - simpleLog_logL(LOG_LEVEL_INFO, "JVM: options:", numOpts); - char dd_rw[2048] = {'\0'}; - callback->DataDirs_locatePath(interfaceId, dd_rw, sizeof(dd_rw), "", true, true, true, false); - size_t i; - - for (i = 0; i < numOpts; ++i) { - char* tmpOptionString = util_allocStrReplaceStr(strOptions[i], "${home-dir}", dd_rw); - - // do not add empty options - if (tmpOptionString == NULL) - continue; - - if (strlen(tmpOptionString) == 0) { - free(tmpOptionString); - tmpOptionString = NULL; - continue; - } - - vm_args->options[vm_args->nOptions ].optionString = tmpOptionString; - vm_args->options[vm_args->nOptions++].extraInfo = NULL; - - simpleLog_logL(LOG_LEVEL_INFO, "JVM option %ul: %s", vm_args->nOptions - 1, tmpOptionString); - } - - simpleLog_logL(LOG_LEVEL_INFO, ""); - return true; -} - - - -static JNIEnv* java_reattachCurrentThread(JavaVM* jvm) -{ - simpleLog_logL(LOG_LEVEL_DEBUG, "Reattaching current thread..."); - - JNIEnv* env = NULL; - - // const jint res = (*jvm)->AttachCurrentThreadAsDaemon(jvm, (void**) &env, NULL); - const jint res = (*jvm)->AttachCurrentThread(jvm, (void**) &env, NULL); - - if (res != 0) { - env = NULL; - simpleLog_logL(LOG_LEVEL_ERROR, "Failed attaching JVM to current thread(2): %i - %s", res, jniUtil_getJniRetValDescription(res)); - } - - return env; -} - -static JNIEnv* java_getJNIEnv(bool preload) -{ - if (g_jvm != NULL) - return java_reattachCurrentThread(g_jvm); - - assert(preload); - simpleLog_logL(LOG_LEVEL_INFO, "Creating the JVM."); - - JNIEnv* ret = NULL; - JNIEnv* env = NULL; - JavaVM* jvm = NULL; - - struct JavaVMInitArgs vm_args; - memset(&vm_args, 0, sizeof(vm_args)); - - jint res = 0; - - if (!java_createJavaVMInitArgs(&vm_args, &jvmCfgProps)) { - simpleLog_logL(LOG_LEVEL_ERROR, "Failed initializing JVM init-arguments."); - goto end; - } - - // Looking for existing JVMs might be problematic: they could - // have been initialized with other JVM-arguments then we need. - // But as we can not use DestroyJavaVM (it makes creating a new - // one for the same process impossible, a (SUN?) JVM limitation) - // we have to do this anyway to support /aireload and /aicontrol - // for Java Skirmish AIs. - // - simpleLog_logL(LOG_LEVEL_INFO, "looking for existing JVMs ..."); - jsize numJVMsFound = 0; - - // grab the first Java VM that has been created - if ((res = JNI_GetCreatedJavaVMs_f(&jvm, 1, &numJVMsFound)) != 0) { - jvm = NULL; - simpleLog_logL(LOG_LEVEL_ERROR, "Failed fetching list of running JVMs: %i - %s", res, jniUtil_getJniRetValDescription(res)); - goto end; - } - - simpleLog_logL(LOG_LEVEL_INFO, "number of existing JVMs: %i", numJVMsFound); - - if (numJVMsFound > 0) { - simpleLog_logL(LOG_LEVEL_INFO, "using an already running JVM."); - } else { - simpleLog_logL(LOG_LEVEL_INFO, "creating JVM..."); - - if ((res = JNI_CreateJavaVM_f(&jvm, (void**) &env, &vm_args)) != 0 || (*env)->ExceptionCheck(env)) { - simpleLog_logL(LOG_LEVEL_ERROR, "Failed to create Java VM: %i - %s", res, jniUtil_getJniRetValDescription(res)); - goto end; - } - } - - // free the JavaVMInitArgs content - jint i; - for (i = 0; i < vm_args.nOptions; ++i) { - FREE(vm_args.options[i].optionString); - } - FREE(vm_args.options); - - //res = (*jvm)->AttachCurrentThreadAsDaemon(jvm, (void**) &env, NULL); - res = (*jvm)->AttachCurrentThread(jvm, (void**) &env, NULL); - - if (res < 0 || (*env)->ExceptionCheck(env)) { - if ((*env)->ExceptionCheck(env)) - (*env)->ExceptionDescribe(env); - - simpleLog_logL(LOG_LEVEL_ERROR, "Failed to attach JVM to current thread: %i - %s", res, jniUtil_getJniRetValDescription(res)); - goto end; - } - -end: - if (env == NULL || jvm == NULL || (*env)->ExceptionCheck(env) || res != 0) { - simpleLog_logL(LOG_LEVEL_ERROR, "JVM: Failed creating."); - - if (env != NULL && (*env)->ExceptionCheck(env)) - (*env)->ExceptionDescribe(env); - - if (jvm != NULL) { - // never destroy the JVM, doing so prevents it from being created again for the same thread - // res = (*jvm)->DestroyJavaVM(jvm); - } - - g_jvm = NULL; - ret = NULL; - } else { - g_jvm = jvm; - ret = env; - } - - return ret; -} - - -bool java_unloadJNIEnv() -{ - if (g_jvm == NULL) - return true; - - simpleLog_logL(LOG_LEVEL_INFO, "JVM: Unloading ..."); - - #if 0 - // JNIEnv* jniEnv = java_getJNIEnv(false); - - // We have to be the ONLY running thread (native and Java) - // this may not help, but will not hurt either - // - // // jint res = (*g_jvm)->AttachCurrentThreadAsDaemon(g_jvm, (void**) &g_jniEnv, NULL); - // jint res = jvm->AttachCurrentThread((void**) &jniEnv, NULL); - #endif - - #if 0 - if (res < 0 || (*g_jniEnv)->ExceptionCheck(g_jniEnv)) { - if ((*g_jniEnv)->ExceptionCheck(g_jniEnv)) - (*g_jniEnv)->ExceptionDescribe(g_jniEnv); - - simpleLog_logL(LOG_LEVEL_ERROR, "JVM: Can not Attach to the current thread: %i - %s", res, jniUtil_getJniRetValDescription(res)); - return false; - } - #endif - - - const jint res = (*g_jvm)->DetachCurrentThread(g_jvm); - - if (res != 0) { - simpleLog_logL(LOG_LEVEL_ERROR, "JVM: Failed detaching current thread: %i - %s", res, jniUtil_getJniRetValDescription(res)); - return false; - } - - // never destroy the JVM because then it can not be created again - // in the same thread; will always fail with return value -1 (see - // java_getJNIEnv) - #if 0 - if ((res = (*g_jvm)->DestroyJavaVM(g_jvm)) != 0) { - simpleLog_logL(LOG_LEVEL_ERROR, "JVM: Failed destroying: %i - %s", res, jniUtil_getJniRetValDescription(res)); - return false; - } - - simpleLog_logL(LOG_LEVEL_NOTICE, "JVM: Successfully destroyed"); - g_jvm = NULL; - #endif - - java_establishSpringEnv(); - return true; -} - - -bool java_initStatic(int _interfaceId, const struct SAIInterfaceCallback* _callback) -{ - interfaceId = _interfaceId; - callback = _callback; - - // Read the jvm properties config file - java_readJvmCfgFile(&jvmCfgProps); - - size_t t; - for (t = 0; t < maxSkirmishAIs; ++t) { - skirmishAIId_skirmishAiImpl[t] = 999999; - } - - size_t sai; - for (sai = 0; sai < maxSkirmishAIs; ++sai) { - jAIClassNames[sai] = NULL; - jAIInstances[sai] = NULL; - jAIClassLoaders[sai] = NULL; - } - - // dynamically load the JVM - char jreLocationFile[2048]; - const bool located = callback->DataDirs_locatePath(interfaceId, jreLocationFile, sizeof(jreLocationFile), JRE_LOCATION_FILE, false, false, false, false); - char jrePath[1024]; - char jvmLibPath[1024]; - - if (!GetJREPath(jrePath, sizeof(jrePath), located ? jreLocationFile : NULL, NULL)) { - simpleLog_logL(LOG_LEVEL_ERROR, "Failed locating a JRE installation, you may specify the JAVA_HOME env var."); - return false; - } - - simpleLog_logL(LOG_LEVEL_NOTICE, "Using JRE (can be changed with JAVA_HOME): %s", jrePath); - -#if defined __arch64__ - static const char* defJvmType = "server"; -#else - static const char* defJvmType = "client"; -#endif - const char* jvmType = java_getValueByKey(&jvmCfgProps, "jvm.type"); - - if (jvmType == NULL) - jvmType = defJvmType; - - if (!GetJVMPath(jrePath, jvmType, jvmLibPath, sizeof(jvmLibPath), NULL)) { - simpleLog_logL(LOG_LEVEL_ERROR, "Failed locating the %s version of the JVM, please contact spring devs.", jvmType); - return false; - } - - if (!sharedLib_isLoaded(jvmSharedLib = sharedLib_load(jvmLibPath))) { - simpleLog_logL(LOG_LEVEL_ERROR, "Failed to load the JVM at \"%s\".", jvmLibPath); - return false; - } - - simpleLog_logL(LOG_LEVEL_NOTICE, "Successfully loaded the JVM shared library at \"%s\".", jvmLibPath); - - if ((JNI_GetDefaultJavaVMInitArgs_f = (JNI_GetDefaultJavaVMInitArgs_t*) sharedLib_findAddress(jvmSharedLib, "JNI_GetDefaultJavaVMInitArgs")) == NULL) { - simpleLog_logL(LOG_LEVEL_ERROR, "Failed to load the JVM, function \"%s\" not exported.", "JNI_GetDefaultJavaVMInitArgs"); - return false; - } - - if ((JNI_CreateJavaVM_f = (JNI_CreateJavaVM_t*) sharedLib_findAddress(jvmSharedLib, "JNI_CreateJavaVM")) == NULL) { - simpleLog_logL(LOG_LEVEL_ERROR, "Failed to load the JVM, function \"%s\" not exported.", "JNI_CreateJavaVM"); - return false; - } - - if ((JNI_GetCreatedJavaVMs_f = (JNI_GetCreatedJavaVMs_t*) sharedLib_findAddress(jvmSharedLib, "JNI_GetCreatedJavaVMs")) == NULL) { - simpleLog_logL(LOG_LEVEL_ERROR, "Failed to load the JVM, function \"%s\" not exported.", "JNI_GetCreatedJavaVMs"); - return false; - } - - java_establishJavaEnv(); - JNIEnv* env = java_getJNIEnv(true); - const bool loaded = (env != NULL); - const bool inited = (loaded && eventsJniBridge_initStatic(env, maxSkirmishAIs) == 0); - java_establishSpringEnv(); - - return inited; -} - -bool java_releaseStatic() -{ - sharedLib_unload(jvmSharedLib); - jvmSharedLib = NULL; - - FREE(jvmCfgProps.keys); - FREE(jvmCfgProps.values); - return true; -} - - - -static jobject java_createAICallback(JNIEnv* env, const struct SSkirmishAICallback* aiCallback, int skirmishAIId) { - (void) aiCallback; - - // initialize the AI Callback class, if not yet done - if (g_cls_aiCallback == NULL) { - // get the AI Callback class - if ((g_cls_aiCallback = jniUtil_findClass(env, CLS_AI_CALLBACK)) == NULL) - return NULL; - if ((g_cls_aiCallback = jniUtil_makeGlobalRef(env, g_cls_aiCallback, CLS_AI_CALLBACK)) == NULL) - return NULL; - // get (int skirmishAIId) constructor - if ((g_m_aiCallback_ctor_I = jniUtil_getMethodID(env, g_cls_aiCallback, "", "(I)V")) == NULL) - return NULL; - } - - #if 1 - // reuse callback if reloading - // this should be safe since the callback objects created in java_skirmishAI_init are not - // broken by java_skirmishAI_handleEvent, which also calls java_reattachCurrentThread and - // mightget a different env-pointer back - if (jAICallBacks[skirmishAIId] != NULL) - return jAICallBacks[skirmishAIId]; - #endif - - jobject o_clb = (*env)->NewObject(env, g_cls_aiCallback, g_m_aiCallback_ctor_I, skirmishAIId); - - if (jniUtil_checkException(env, "Failed creating Java AI Callback instance")) - return NULL; - - // return (jAICallBacks[skirmishAIId] = jniUtil_makeGlobalRef(env, o_clb, "AI callback instance")); - return (jAICallBacks[skirmishAIId] = o_clb); -} - - - -static bool java_loadSkirmishAI( - JNIEnv* env, - const char* shortName, - const char* version, - const char* className, - jobject* o_ai, - jobject* o_aiClassLoader -) { - #if 0 - // convert className from "com.myai.AI" to "com/myai/AI" - const size_t classNameP_sizeMax = strlen(className) + 1; - char classNameP[classNameP_sizeMax]; - STRCPY_T(classNameP, classNameP_sizeMax, className); - util_strReplaceChar(classNameP, '.', '/'); - #endif - - // get the AIs private class-loader - jobject o_global_aiClassLoader = java_createAIClassLoader(env, shortName, version); - - if (o_global_aiClassLoader == NULL) - return false; - - *o_aiClassLoader = o_global_aiClassLoader; - - // get the AI interface (from AIInterface.jar) - if (g_cls_ai_int == NULL) { - if ((g_cls_ai_int = jniUtil_findClass(env, INT_AI)) == NULL) - return false; - if ((g_cls_ai_int = jniUtil_makeGlobalRef(env, g_cls_ai_int, "AI interface class")) == NULL) - return false; - } - - // get the AI implementation class (from SkirmishAI.jar) - jclass cls_ai = jniUtil_findClassThroughLoader(env, o_global_aiClassLoader, className); - - if (cls_ai == NULL) - return false; - - const bool implementsAIInt = (bool) (*env)->IsAssignableFrom(env, cls_ai, g_cls_ai_int); - - if (!implementsAIInt || (*env)->ExceptionCheck(env)) { - simpleLog_logL(LOG_LEVEL_ERROR, "AI class not assignable from interface "INT_AI": %s", className); - simpleLog_logL(LOG_LEVEL_ERROR, "possible reasons (this list could be incomplete):"); - simpleLog_logL(LOG_LEVEL_ERROR, "* "INT_AI" interface not implemented"); - simpleLog_logL(LOG_LEVEL_ERROR, "* The AI is not compiled for the Java AI Interface version in use"); - - if (implementsAIInt) - (*env)->ExceptionDescribe(env); - - return false; - } - - - // get factory no-arg ctor - jmethodID m_ai_ctor = jniUtil_getMethodID(env, cls_ai, "", "()V"); - - if (m_ai_ctor == NULL) - return false; - - - // get AI instance - jobject o_local_ai = (*env)->NewObject(env, cls_ai, m_ai_ctor); - - if (o_local_ai == NULL || (*env)->ExceptionCheck(env)) { - simpleLog_logL(LOG_LEVEL_ERROR, "Failed fetching AI instance for class: %s", className); - - if (o_local_ai != NULL) - (*env)->ExceptionDescribe(env); - - return false; - } - - // make the AI a global reference, so it will not be garbage collected even after this method returns - *o_ai = jniUtil_makeGlobalRef(env, o_local_ai, "AI instance"); - return true; -} - - -bool java_initSkirmishAIClass( - const char* const shortName, - const char* const version, - const char* const className, - int skirmishAIId -) { - bool success = false; - - // see if an AI for className is instantiated already - size_t sai; - size_t firstFree = numSkirmishAIs; - - for (sai = 0; sai < numSkirmishAIs; ++sai) { - if (jAIClassNames[sai] == NULL) { - firstFree = sai; - break; - } - } - - // sai is now either the instantiated one, or a free one - // instantiate AI (if not already instantiated) - assert(sai < maxSkirmishAIs); - - if (jAIClassNames[sai] == NULL) { - sai = firstFree; - java_establishJavaEnv(); - JNIEnv* env = java_getJNIEnv(false); - - jobject instance = NULL; - jobject classLoader = NULL; - - success = java_loadSkirmishAI(env, shortName, version, className, &instance, &classLoader); - java_establishSpringEnv(); - - if (success) { - jAIInstances[sai] = instance; - jAIClassLoaders[sai] = classLoader; - jAIClassNames[sai] = util_allocStrCpy(className); - - numSkirmishAIs += (firstFree == numSkirmishAIs); - } else { - simpleLog_logL(LOG_LEVEL_ERROR, "Class loading failed for class: %s", className); - } - } else { - success = true; - } - - if (success) - skirmishAIId_skirmishAiImpl[skirmishAIId] = sai; - - return success; -} - -bool java_releaseSkirmishAIClass(const char* className) -{ - // see if an AI for className is instantiated - size_t sai; - for (sai = 0; sai < numSkirmishAIs; ++sai) { - if (jAIClassNames[sai] == NULL) - continue; - if (strcmp(jAIClassNames[sai], className) == 0) - break; - } - - // sai is now either the instantiated one, or a free one - // release AI (if its instance was found) - assert(sai < maxSkirmishAIs); - - if (jAIClassNames[sai] == NULL) - return false; - - java_establishJavaEnv(); - JNIEnv* env = java_getJNIEnv(false); - - - // delete the AI class-loader global reference, - // so it will be garbage collected - bool successPart = jniUtil_deleteGlobalRef(env, jAIClassLoaders[sai], "AI class-loader"); - bool success = successPart; - - // delete the AI global reference, - // so it will be garbage collected - successPart = jniUtil_deleteGlobalRef(env, jAIInstances[sai], "AI instance"); - success = success && successPart; - - java_establishSpringEnv(); - - if (success) { - jAIClassLoaders[sai] = NULL; - jAIInstances[sai] = NULL; - - FREE(jAIClassNames[sai]); - - // if it is the last implementation - numSkirmishAIs -= ((sai + 1) == numSkirmishAIs); - } - - return success; -} - -bool java_releaseAllSkirmishAIClasses() -{ - bool success = true; - - const char* className = ""; - size_t sai; - - for (sai = 0; sai < numSkirmishAIs; ++sai) { - if ((className = jAIClassNames[sai]) == NULL) - continue; - - success = success && java_releaseSkirmishAIClass(className); - } - - return success; -} - - - -int java_skirmishAI_init(int skirmishAIId, const struct SSkirmishAICallback* aiCallback) -{ - int res = -1; - - java_establishJavaEnv(); - - JNIEnv* env = java_getJNIEnv(false); - jobject global_javaAICallback = java_createAICallback(env, aiCallback, skirmishAIId); - - if (global_javaAICallback != NULL) - res = eventsJniBridge_initAI(env, skirmishAIId, global_javaAICallback); - - java_establishSpringEnv(); - return res; -} - -int java_skirmishAI_release(int skirmishAIId) -{ - return 0; -} - -int java_skirmishAI_handleEvent(int skirmishAIId, int topic, const void* data) -{ - java_establishJavaEnv(); - - JNIEnv* env = java_getJNIEnv(false); - const size_t sai = skirmishAIId_skirmishAiImpl[skirmishAIId]; - jobject aiInstance = jAIInstances[sai]; - const int res = eventsJniBridge_handleEvent(env, aiInstance, skirmishAIId, topic, data); - - java_establishSpringEnv(); - return res; -} diff --git a/AI/Interfaces/Java/src/main/native/JavaBridge.h b/AI/Interfaces/Java/src/main/native/JavaBridge.h deleted file mode 100644 index d742f47ef38..00000000000 --- a/AI/Interfaces/Java/src/main/native/JavaBridge.h +++ /dev/null @@ -1,73 +0,0 @@ -/* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ - -#ifndef _JAVA_BRIDGE_H -#define _JAVA_BRIDGE_H - -#define JVM_PROPERTIES_FILE "jvm.properties" - -#define PKG_AI "com/springrts/ai/" -#define INT_AI PKG_AI"AI" -#define CLS_AI_CALLBACK PKG_AI"JniAICallback" - -// define path entry delimiter, used eg for the java class-path -#ifdef _WIN32 -#define ENTRY_DELIM ";" -#else -#define ENTRY_DELIM ":" -#endif -#define PATH_DELIM "/" - -#ifdef __cplusplus -extern "C" { -#endif - -#include // bool, true, false - -struct SAIInterfaceCallback; -struct SSkirmishAICallback; - -bool java_unloadJNIEnv(); - -bool java_initStatic(int interfaceId, const struct SAIInterfaceCallback* callback); -bool java_releaseStatic(); - -/** - * Instantiates an instance of the specified className. - * - * @param shortName further specifies the the AI to load - * @param version further specifies the the AI to load - * @param className fully qualified name of a Java class that implements - * interface com.springrts.ai.AI, eg: - * "com.myai.AI" - * @param teamId The team that will be using this AI. - * Multiple teams may use the same AI implementation. - * @return true, if the AI implementation is now loaded - */ -bool java_initSkirmishAIClass( - const char* const shortName, - const char* const version, - const char* const className, - int teamId -); - -/** - * Release the loaded AI specified through a class name. - * - * @param className fully qualified name of a Java class that implements - * interface com.springrts.ai.AI, eg: - * "com.myai.AI" - * @return true, if the AI implementation was loaded and is now - * successfully unloaded - */ -bool java_releaseSkirmishAIClass(const char* className); -bool java_releaseAllSkirmishAIClasses(); - -int java_skirmishAI_init(int teamId, const struct SSkirmishAICallback* callback); -int java_skirmishAI_release(int teamId); -int java_skirmishAI_handleEvent(int teamId, int topic, const void* data); - -#ifdef __cplusplus -} // extern "C" -#endif - -#endif // _JAVA_BRIDGE_H diff --git a/AI/Interfaces/Java/src/main/native/JniUtil.c b/AI/Interfaces/Java/src/main/native/JniUtil.c deleted file mode 100644 index 909afc70bd3..00000000000 --- a/AI/Interfaces/Java/src/main/native/JniUtil.c +++ /dev/null @@ -1,288 +0,0 @@ -/* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ - -#include "JniUtil.h" - -#include "CUtils/SimpleLog.h" - - -// JNI global vars -static jclass g_cls_url = NULL; -static jmethodID g_m_url_ctor = NULL; - -static jclass g_cls_urlClassLoader = NULL; -static jmethodID g_m_urlClassLoader_ctor = NULL; -static jmethodID g_m_urlClassLoader_findClass = NULL; - - -const char* jniUtil_getJniRetValDescription(const jint retVal) { - - switch (retVal) { - case JNI_OK: { return "JNI_OK - success"; break; } - case JNI_ERR: { return "JNI_ERR - unknown error"; break; } - case JNI_EDETACHED: { return "JNI_EDETACHED - thread detached from the VM"; break; } - case JNI_EVERSION: { return "JNI_EVERSION - JNI version error"; break; } -#ifdef JNI_ENOMEM - case JNI_ENOMEM: { return "JNI_ENOMEM - not enough (contiguous) memory"; break; } -#endif // JNI_ENOMEM -#ifdef JNI_EEXIST - case JNI_EEXIST: { return "JNI_EEXIST - VM already created"; break; } -#endif // JNI_EEXIST -#ifdef JNI_EINVAL - case JNI_EINVAL: { return "JNI_EINVAL - invalid arguments"; break; } -#endif // JNI_EINVAL - default: { return "UNKNOWN - unknown/invalid JNI return value"; break; } - } -} - -bool jniUtil_checkException(JNIEnv* env, const char* const errorMsg) { - - if ((*env)->ExceptionCheck(env)) { - simpleLog_logL(LOG_LEVEL_ERROR, errorMsg); - (*env)->ExceptionDescribe(env); - return true; - } - - return false; -} - -jclass jniUtil_findClass(JNIEnv* env, const char* const className) { - - jclass res = NULL; - - res = (*env)->FindClass(env, className); - const bool hasException = (*env)->ExceptionCheck(env); - if (res == NULL || hasException) { - simpleLog_logL(LOG_LEVEL_ERROR, "Class not found: \"%s\"", className); - if (hasException) { - (*env)->ExceptionDescribe(env); - } - res = NULL; - } - - return res; -} - -jobject jniUtil_makeGlobalRef(JNIEnv* env, jobject localObject, const char* objDesc) { - - jobject res = NULL; - - // Make the local class a global reference, - // so it will not be garbage collected, - // even after this method returned, - // but only if explicitly deleted with DeleteGlobalRef - res = (*env)->NewGlobalRef(env, localObject); - if ((*env)->ExceptionCheck(env)) { - simpleLog_logL(LOG_LEVEL_ERROR, - "Failed to make %s a global reference.", - ((objDesc == NULL) ? "" : objDesc)); - (*env)->ExceptionDescribe(env); - res = NULL; - } - - return res; -} - -bool jniUtil_deleteGlobalRef(JNIEnv* env, jobject globalObject, - const char* objDesc) { - - // delete the AI class-loader global reference, - // so it will be garbage collected - (*env)->DeleteGlobalRef(env, globalObject); - if ((*env)->ExceptionCheck(env)) { - simpleLog_logL(LOG_LEVEL_ERROR, - "Failed to delete global reference %s.", - ((objDesc == NULL) ? "" : objDesc)); - (*env)->ExceptionDescribe(env); - return false; - } - - return true; -} - -jmethodID jniUtil_getMethodID(JNIEnv* env, jclass cls, - const char* const name, const char* const signature) { - - jmethodID res = NULL; - - res = (*env)->GetMethodID(env, cls, name, signature); - const bool hasException = (*env)->ExceptionCheck(env); - if (res == NULL || hasException) { - simpleLog_logL(LOG_LEVEL_ERROR, "Method not found: %s(%s)", - name, signature); - if (hasException) { - (*env)->ExceptionDescribe(env); - } - res = NULL; - } - - return res; -} - -jmethodID jniUtil_getStaticMethodID(JNIEnv* env, jclass cls, - const char* const name, const char* const signature) { - - jmethodID res = NULL; - - res = (*env)->GetStaticMethodID(env, cls, name, signature); - const bool hasException = (*env)->ExceptionCheck(env); - if (res == NULL || hasException) { - simpleLog_logL(LOG_LEVEL_ERROR, "Method not found: %s(%s)", - name, signature); - if (hasException) { - (*env)->ExceptionDescribe(env); - } - res = NULL; - } - - return res; -} - - -static bool jniUtil_initURLClass(JNIEnv* env) { - - if (g_m_url_ctor == NULL) { - // get the URL class - static const char* const fcCls = "java/net/URL"; - - g_cls_url = jniUtil_findClass(env, fcCls); - if (g_cls_url == NULL) return false; - - g_cls_url = jniUtil_makeGlobalRef(env, g_cls_url, fcCls); - if (g_cls_url == NULL) return false; - - // get (String) constructor - g_m_url_ctor = jniUtil_getMethodID(env, g_cls_url, - "", "(Ljava/lang/String;)V"); - if (g_m_url_ctor == NULL) return false; - } - - return true; -} -jobject jniUtil_createURLObject(JNIEnv* env, const char* const url) { - - jobject jurl = NULL; - - bool ok = true; - if (g_cls_url == NULL) { - ok = jniUtil_initURLClass(env); - } - - if (ok) { - jstring jstrUrl = (*env)->NewStringUTF(env, url); - if (jniUtil_checkException(env, "Failed creating Java String.")) { jstrUrl = NULL; } - if (jstrUrl != NULL) { - jurl = (*env)->NewObject(env, g_cls_url, g_m_url_ctor, jstrUrl); - if (jniUtil_checkException(env, "Failed creating Java URL.")) { jurl = NULL; } - } - } else { - simpleLog_logL(LOG_LEVEL_ERROR, - "Failed creating Java URL; URL class not initialized."); - } - - return jurl; -} -jobjectArray jniUtil_createURLArray(JNIEnv* env, size_t size) { - - jobjectArray jurlArr = NULL; - - bool ok = true; - if (g_cls_url == NULL) { - ok = jniUtil_initURLClass(env); - } - - if (ok) { - jurlArr = (*env)->NewObjectArray(env, size, g_cls_url, NULL); - if (jniUtil_checkException(env, "Failed creating URL[].")) { jurlArr = NULL; } - } else { - simpleLog_logL(LOG_LEVEL_ERROR, - "Failed creating Java URL[]; URL class not initialized."); - } - - return jurlArr; -} -bool jniUtil_insertURLIntoArray(JNIEnv* env, jobjectArray arr, size_t index, jobject url) { - - bool ok = true; - - (*env)->SetObjectArrayElement(env, arr, index, url); - if (jniUtil_checkException(env, "Failed inserting Java URL into array.")) { ok = false; } - - return ok; -} - -static bool jniUtil_initURLClassLoaderClass(JNIEnv* env) { - - if (g_m_urlClassLoader_findClass == NULL) { - // get the URLClassLoader class - static const char* const fcCls = "java/net/URLClassLoader"; - - g_cls_urlClassLoader = jniUtil_findClass(env, fcCls); - if (g_cls_urlClassLoader == NULL) return false; - - g_cls_urlClassLoader = - jniUtil_makeGlobalRef(env, g_cls_urlClassLoader, fcCls); - if (g_cls_urlClassLoader == NULL) return false; - - // get (URL[]) constructor - g_m_urlClassLoader_ctor = jniUtil_getMethodID(env, - g_cls_urlClassLoader, "", "([Ljava/net/URL;)V"); - if (g_m_urlClassLoader_ctor == NULL) return false; - - // get the findClass(String) method - g_m_urlClassLoader_findClass = jniUtil_getMethodID(env, - g_cls_urlClassLoader, "findClass", - "(Ljava/lang/String;)Ljava/lang/Class;"); - if (g_m_urlClassLoader_findClass == NULL) return false; - } - - return true; -} -jobject jniUtil_createURLClassLoader(JNIEnv* env, jobject urlArray) { - - jobject classLoader = NULL; - - bool ok = true; - if (g_m_urlClassLoader_ctor == NULL) { - ok = jniUtil_initURLClassLoaderClass(env); - } - - if (ok) { - classLoader = (*env)->NewObject(env, g_cls_urlClassLoader, g_m_urlClassLoader_ctor, urlArray); - if (jniUtil_checkException(env, "Failed creating class-loader.")) { return NULL; } - } else { - simpleLog_logL(LOG_LEVEL_ERROR, - "Failed creating class-loader; class-loader class not initialized."); - } - - return classLoader; -} -jclass jniUtil_findClassThroughLoader(JNIEnv* env, jobject classLoader, const char* const className) { - - jclass cls = NULL; - - bool ok = true; - if (g_m_urlClassLoader_findClass == NULL) { - ok = jniUtil_initURLClassLoaderClass(env); - } - - if (ok) { - //cls = (*env)->FindClass(env, classNameP); - jstring jstr_className = (*env)->NewStringUTF(env, className); - cls = (*env)->CallObjectMethod(env, classLoader, g_m_urlClassLoader_findClass, jstr_className); - const bool hasException = (*env)->ExceptionCheck(env); - if (cls == NULL || hasException) { - simpleLog_logL(LOG_LEVEL_ERROR, - "Class not found \"%s\"", className); - if (hasException) { - (*env)->ExceptionDescribe(env); - } - cls = NULL; - } - } else { - simpleLog_logL(LOG_LEVEL_ERROR, - "Failed finding class; class-loader class not initialized."); - } - - return cls; -} - diff --git a/AI/Interfaces/Java/src/main/native/JniUtil.h b/AI/Interfaces/Java/src/main/native/JniUtil.h deleted file mode 100644 index 336f2fc4f1e..00000000000 --- a/AI/Interfaces/Java/src/main/native/JniUtil.h +++ /dev/null @@ -1,80 +0,0 @@ -/* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ - -#ifndef _JNI_UTIL_H -#define _JNI_UTIL_H - -#ifdef __cplusplus -extern "C" { -#endif - -#include // bool, true, false - -#include - -/** - * Takes a JNI function return value, and returns a short description for it. - * This can be used for getting a human readable string describing the - * return value of functions like AttachCurrentThread() and CreateJavaVM(). - * - * @return a short description for a JNI function return value - */ -const char* jniUtil_getJniRetValDescription(const jint retVal); - -/** - * Handles a possible JNI/Java exception. - * In case an exception is present, the supplied error message is written to - * the log file, and the whole exception info is written to stderr. - * - * @return true, if there was an exception - */ -bool jniUtil_checkException(JNIEnv* env, const char* const errorMsg); - -/** - * Tries to find a Java class. - * - * @return a local reference to the class if found, NULL otherwise - */ -jclass jniUtil_findClass(JNIEnv* env, const char* const className); - -/** - * Converts a local to a global reference. - * As jclass inherits from jobject, this can be used for jclass too. - * - * @return the global reference to localObject on success, NULL otherwise - */ -jobject jniUtil_makeGlobalRef(JNIEnv* env, jobject localObject, const char* objDesc); - -/** - * Deletes a global reference to an object. - * As jclass inherits from jobject, this can be used for jclass too. - * - * @return true on success, false if an error occurred - */ -bool jniUtil_deleteGlobalRef(JNIEnv* env, jobject globalObject, const char* objDesc); - -/** - * Retrieves a reference ID for calling a Java object method. - * - * @return the method ID on success, NULL otherwise - */ -jmethodID jniUtil_getMethodID(JNIEnv* env, jclass cls, const char* const name, const char* const signature); - -/** - * Retrieves a reference ID for calling a Java static method. - * - * @return the method ID on success, NULL otherwise - */ -jmethodID jniUtil_getStaticMethodID(JNIEnv* env, jclass cls, const char* const name, const char* const signature); - -jobject jniUtil_createURLObject(JNIEnv* env, const char* const url); -jobjectArray jniUtil_createURLArray(JNIEnv* env, size_t size); -bool jniUtil_insertURLIntoArray(JNIEnv* env, jobjectArray arr, size_t index, jobject url); - -jobject jniUtil_createURLClassLoader(JNIEnv* env, jobject urlArray); -jclass jniUtil_findClassThroughLoader(JNIEnv* env, jobject classLoader, const char* const className); - -#ifdef __cplusplus -} // extern "C" -#endif - -#endif // _JNI_UTIL_H diff --git a/AI/Interfaces/Java/src/main/native/JvmLocater.h b/AI/Interfaces/Java/src/main/native/JvmLocater.h deleted file mode 100644 index 5032991d6a0..00000000000 --- a/AI/Interfaces/Java/src/main/native/JvmLocater.h +++ /dev/null @@ -1,53 +0,0 @@ -/* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ - -#ifndef _JVM_LOCATER_H -#define _JVM_LOCATER_H - -#if !defined bool -#include -#endif -#if !defined size_t -#include -#endif - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * Returns the arch dir name, used eg here: - * .../jre/lib/${arch}/client/libjvm.so - * - * @return sparc, sparcv9, i386, amd64 or ia64 (windows only) - */ -const char* GetArchPath(); - -/* - * Given a JRE location and a JVM type, construct what the name the - * JVM shared library will be. - * - * @param jvmType "/", "\\", "client", "server" - * @param arch see GetArchPath(), use NULL for the default value - * @return true, if the JVM library was found, false otherwise. - */ -bool GetJVMPath(const char* jrePath, const char* jvmType, - char* jvmPath, size_t jvmPathSize, const char* arch); - -/** - * Find the path to a JRE install dir, using platform dependent means. - * - * @param path path of the JRE installation - * @param pathSize size of the path parameter - * @param configFile path to a simple text file containing only a path to the - * JRE installation to use, or NULL - * @param arch see GetArchPath(), use NULL for the default value - * @return true, if a JRE was found, false otherwise. - */ -bool GetJREPath(char* path, size_t pathSize, const char* configFile, - const char* arch); - -#ifdef __cplusplus -} // extern "C" -#endif - -#endif // _JVM_LOCATER_H diff --git a/AI/Interfaces/Java/src/main/native/JvmLocater_common.c b/AI/Interfaces/Java/src/main/native/JvmLocater_common.c deleted file mode 100644 index a85f9d87a95..00000000000 --- a/AI/Interfaces/Java/src/main/native/JvmLocater_common.c +++ /dev/null @@ -1,188 +0,0 @@ -/* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ - -#include -#include -#include -#include -#include - -#include "CUtils/Util.h" - -#include "JvmLocater.h" - -#define MAXPATHLEN 2048 -#define JRE_PATH_PROPERTY "jre.path" - -#include // for CHAR_BIT -#define CURRENT_DATA_MODEL (CHAR_BIT * sizeof(void*)) - -#include "CUtils/SimpleLog.h" -#include "System/MainDefines.h" -#include "System/SafeCStrings.h" - -// implemented in the OS specific files -const char* GetArchPath(); -bool GetJREPathFromBase(char* path, size_t pathSize, const char* basePath, - const char* arch); -bool GetJREPathOSSpecific(char* path, size_t pathSize, const char* arch); - -bool FileExists(const char* filePath) -{ - struct stat s; - return (stat(filePath, &s) == 0); -} - -//#define LOC_PROP_FILE -bool GetJREPathFromConfig(char* path, size_t pathSize, const char* configFile) -{ -#if defined LOC_PROP_FILE - // assume the config file is in properties file format - // and that the property JRE_PATH_PROPERTY contains - // the absolute path to the JRE to use - static const size_t props_sizeMax = 64; - - const char* props_keys[props_sizeMax]; - const char* props_values[props_sizeMax]; - - const size_t props_size = util_parsePropertiesFile(configFile, props_keys, - props_values, props_sizeMax); - - const char* jvmLocation = util_map_getValueByKey( - props_size, props_keys, props_values, - JRE_PATH_PROPERTY); - - if (jvmLocation == NULL) { - simpleLog_logL(LOG_LEVEL_DEBUG, "JRE not found in config file!"); - return false; - } else { - simpleLog_logL(LOG_LEVEL_NOTICE, "JRE found in config file!"); - STRCPY_T(path, pathSize, jvmLocation); - return true; - } -#else // defined LOC_PROP_FILE - // assume the config file is a plain text file containing nothing but - // the absolute path to the JRE to use in the first line - - bool found = false; - - FILE* cfp = fopen(configFile, "r"); - if (cfp == NULL) { - return found; - } - - // parse results - static const size_t line_sizeMax = 1024; - char line[line_sizeMax]; - if (fgets(line, line_sizeMax, cfp)) { - size_t line_size = strlen(line); - if (*(line+line_size-1) == '\n') { - // remove trailing '\n' - *(line+line_size-1) = '\0'; - line_size--; - } - - simpleLog_logL(LOG_LEVEL_NOTICE, - "Fetched JRE location from \"%s\"!", configFile); - - if (line_size > 0 && *line == '/') { - *(line+line_size-1) = '\0'; // remove trailing '/' - } - STRCPY_T(path, pathSize, line); - found = true; - } - fclose(cfp); - - return found; -#endif // defined LOC_PROP_FILE -} - - -bool GetJREPathFromEnvVars(char* path, size_t pathSize, const char* arch) -{ - bool found = false; - - static const size_t possLoc_sizeMax = 32; - char* possLoc[possLoc_sizeMax]; - size_t possLoc_i = 0; - - possLoc[possLoc_i++] = util_allocStrCpy("JAVA_HOME"); - possLoc[possLoc_i++] = util_allocStrCpy("JDK_HOME"); - possLoc[possLoc_i++] = util_allocStrCpy("JRE_HOME"); - - size_t l; - for (l=0; l < possLoc_i; ++l) { - const char* envPath = getenv(possLoc[l]); - if (envPath != NULL) { - found = GetJREPathFromBase(path, pathSize, envPath, arch); - if (found) { - simpleLog_logL(LOG_LEVEL_NOTICE, "JRE found in env var \"%s\"!", possLoc[l]); - goto locSearchEnd; - } else { - simpleLog_logL(LOG_LEVEL_WARNING, "Unusable JRE from env var \"%s\"=\"%s\"!", possLoc[l], envPath); - } - } - } - locSearchEnd: - - // cleanup - for (l=0; l < possLoc_i; ++l) { - free(possLoc[l]); - possLoc[l] = NULL; - } - - return found; -} - - - -bool GetJREPath(char* path, size_t pathSize, const char* configFile, - const char* arch) -{ - bool found = false; - - if (arch == NULL) { - arch = GetArchPath(); - } - - // check if a JRE location is specified in the config file - if (!found && configFile != NULL) { - found = GetJREPathFromConfig(path, pathSize, configFile); - } - - // check if a JRE is specified in an ENV var (eg. JAVA_HOME) - if (!found) { - found = GetJREPathFromEnvVars(path, pathSize, arch); - } - - // check if a JRE is located in a common location - if (!found) { - found = GetJREPathOSSpecific(path, pathSize, arch); - } - - return found; -} - -int main(int argc, const char* argv[]) { - - //simpleLog_init(NULL, false, LOG_LEVEL_DEBUG, false); - - static const size_t path_sizeMax = 1024; - char path[path_sizeMax]; - bool found = GetJREPath(path, path_sizeMax, NULL, NULL); - if (found) { - printf("JRE found: %s\n", path); - - static const size_t jvmPath_sizeMax = 1024; - char jvmPath[jvmPath_sizeMax]; - bool jvmFound = GetJVMPath(path, "client", jvmPath, jvmPath_sizeMax, NULL); - if (jvmFound) { - printf("JVM found: %s\n", jvmPath); - } else { - printf("JVM not found.\n"); - } - } else { - printf("JRE not found.\n"); - } - - return 0; -} diff --git a/AI/Interfaces/Java/src/main/native/JvmLocater_common.h b/AI/Interfaces/Java/src/main/native/JvmLocater_common.h deleted file mode 100644 index 45d81c2b9ac..00000000000 --- a/AI/Interfaces/Java/src/main/native/JvmLocater_common.h +++ /dev/null @@ -1,38 +0,0 @@ -/* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ - -#ifndef _JVM_LOCATER_COMMON_H -#define _JVM_LOCATER_COMMON_H - -#ifdef __cplusplus -extern "C" { -#endif - -#include "JvmLocater.h" - -#include -#include -#include -#include -#include -#include // for CHAR_BIT - -#include "CUtils/Util.h" -#include "CUtils/SimpleLog.h" - -#define MAXPATHLEN 2048 -#define JRE_PATH_PROPERTY "jre.path" -#define CURRENT_DATA_MODEL (CHAR_BIT * sizeof(void*)) - - -bool FileExists(const char* filePath); - -bool GetJREPathFromEnvVars(char* path, size_t pathSize, const char* arch); - -bool GetJREPath(char* path, size_t pathSize, const char* configFile, - const char* arch); - -#ifdef __cplusplus -} // extern "C" -#endif - -#endif // _JVM_LOCATER_COMMON_H diff --git a/AI/Interfaces/Java/src/main/native/JvmLocater_linux.c b/AI/Interfaces/Java/src/main/native/JvmLocater_linux.c deleted file mode 100644 index ef1896be34b..00000000000 --- a/AI/Interfaces/Java/src/main/native/JvmLocater_linux.c +++ /dev/null @@ -1,266 +0,0 @@ -/* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ - -#if !defined _WIN32 - -#include "JvmLocater_common.h" - -#include "System/MainDefines.h" -#include "System/SafeCStrings.h" - -#include - - -#if defined APPLE -#define JVM_LIB "libjvm.dylib" -#else -#define JVM_LIB "libjvm.so" -#endif - -#if defined APPLE -#define JAVA_LIB "libjava.dylib" -#else -#define JAVA_LIB "libjava.so" -#endif - -#ifdef SPARC -# define LIBARCH32NAME "sparc" -# define LIBARCH64NAME "sparcv9" -#else // not SPARC -> LINUX or APPLE -# define LIBARCH32NAME "i386" -# define LIBARCH64NAME "amd64" -#endif // SPARC -#if defined __arch64__ -# define LIBARCHNAME LIBARCH64NAME -#else // defined __arch64__ -# define LIBARCHNAME LIBARCH32NAME -#endif // defined __arch64__ -//LIBARCH32 solaris only: sparc or i386 -//LIBARCH64 solaris only: sparcv9 or amd64 -//LIBARCH sparc, sparcv9, i386, amd64, or ia64 // last one is windows only - - -const char* GetArchPath() -{ - switch(CURRENT_DATA_MODEL) { -#ifdef DUAL_MODE - case 32: - return LIBARCH32NAME; - case 64: - return LIBARCH64NAME; -#endif // DUAL_MODE - default: - return LIBARCHNAME; - } -} - -/* - * On Solaris VM choosing is done by the launcher (java.c). - */ -bool GetJVMPath(const char* jrePath, const char* jvmType, - char* jvmPath, size_t jvmPathSize, const char* arch) -{ - if (arch == NULL) { - arch = GetArchPath(); - } - - if (*jvmType == '/') { - SNPRINTF(jvmPath, jvmPathSize, "%s/"JVM_LIB, jvmType); - } else { - SNPRINTF(jvmPath, jvmPathSize, "%s/lib/%s/%s/"JVM_LIB, jrePath, arch, - jvmType); - } - - return FileExists(jvmPath); -} - - -static bool CheckIfJREPath(const char* path, const char* arch) -{ - bool found = false; - - if (path != NULL) { - char libJava[MAXPATHLEN]; - - // Is path a JRE path? - SNPRINTF(libJava, MAXPATHLEN, "%s/lib/%s/"JAVA_LIB, path, arch); - if (access(libJava, F_OK) == 0) { - found = true; - } - } - - return found; -} - -bool GetJREPathFromBase(char* path, size_t pathSize, const char* basePath, - const char* arch) -{ - bool found = false; - - if (basePath != NULL) { - char jrePath[MAXPATHLEN]; - - // Is basePath a JRE path? - STRCPY_T(jrePath, MAXPATHLEN, basePath); - if (CheckIfJREPath(jrePath, arch)) { - STRCPY_T(path, pathSize, basePath); - found = true; - } - - // Is basePath/jre a JRE path? - STRCAT_T(jrePath, MAXPATHLEN, "/jre"); - if (CheckIfJREPath(jrePath, arch)) { - STRCPY_T(path, pathSize, jrePath); - found = true; - } - } - - return found; -} - -static size_t ExecFileSystemGlob(char** pathHits, size_t pathHits_sizeMax, - const char* globPattern) -{ - size_t pathHits_size = 0; - - // assemble the command - static const size_t cmd_sizeMax = 512; - char cmd[cmd_sizeMax]; - SNPRINTF(cmd, cmd_sizeMax, "find %s/ -maxdepth 0 2> /dev/null", globPattern); - - // execute - FILE* cmd_fp = popen(cmd, "r"); - if (cmd_fp == NULL) { - return pathHits_size; - } - - // parse results - static const size_t line_sizeMax = 512; - char line[line_sizeMax]; - while (fgets(line, line_sizeMax, cmd_fp) && (pathHits_size < pathHits_sizeMax)) { - size_t line_size = strlen(line); - if (*(line+line_size-1) == '\n') { - // remove trailing '\n' - *(line+line_size-1) = '\0'; - line_size--; - } - - simpleLog_logL(LOG_LEVEL_DEBUG, - "glob-hit \"%s\"!", line); - - if (line_size > 0 && *line == '/') { - *(line+line_size-1) = '\0'; // remove trailing '/' - pathHits[pathHits_size++] = util_allocStrCpy(line); - } - } - pclose(cmd_fp); - - return pathHits_size; -} -static bool GetJREPathInCommonLocations(char* path, size_t pathSize, const char* arch) -{ - bool found = false; - - static const size_t possLoc_sizeMax = 32; - char* possLoc[possLoc_sizeMax]; - size_t possLoc_i = 0; - - possLoc[possLoc_i++] = util_allocStrCpy("/usr/local/jdk*"); - possLoc[possLoc_i++] = util_allocStrCpy("/usr/lib/jvm/default-java"); - possLoc[possLoc_i++] = util_allocStrCpy("/usr/lib/jvm/java-?-sun"); - possLoc[possLoc_i++] = util_allocStrCpy("/usr/lib/jvm/java-?-*"); - possLoc[possLoc_i++] = util_allocStrCpy("~/jdk*"); - possLoc[possLoc_i++] = util_allocStrCpy("~/bin/jdk*"); - possLoc[possLoc_i++] = util_allocStrCpy("~/jre*"); - possLoc[possLoc_i++] = util_allocStrCpy("~/bin/jre*"); - - static const size_t globHits_sizeMax = 32; - char* globHits[globHits_sizeMax]; - size_t l, g; - for (l=0; l < possLoc_i; ++l) { - const size_t globHits_size = ExecFileSystemGlob(globHits, globHits_sizeMax, possLoc[l]); - for (g=0; g < globHits_size; ++g) { - found = GetJREPathFromBase(path, pathSize, globHits[g], arch); - if (found) { - simpleLog_logL(LOG_LEVEL_NOTICE, - "JRE found common location env var \"%s\"!", - possLoc[l]); - goto locSearchEnd; - } - } - } - locSearchEnd: - - // cleanup - for (l=0; l < possLoc_i; ++l) { - free(possLoc[l]); - possLoc[l] = NULL; - } - - return found; -} - -static bool GetJREPathWhichJava(char* path, size_t pathSize, const char* arch) -{ - static const char* suf = "/bin/java"; - const size_t suf_size = strlen(suf); - - bool found = false; - - // execute - FILE* cmd_fp = popen("which java | sed 's/[\\n\\r]/K/g'", "r"); - if (cmd_fp == NULL) { - return found; - } - - // parse results - static const size_t line_sizeMax = 512; - char line[line_sizeMax]; - if (fgets(line, line_sizeMax, cmd_fp)) { - if (*line == '/') { // -> absolute path - size_t line_size = strlen(line); - if (*(line+line_size-1) == '\n') { - // remove trailing '\n' - *(line+line_size-1) = '\0'; - line_size--; - } - - simpleLog_logL(LOG_LEVEL_DEBUG, - "which line \"%s\"!", line); - - if (line_size > suf_size - && strcmp(line+(line_size-suf_size), suf) == 0) { - // line ends with suf - simpleLog_logL(LOG_LEVEL_NOTICE, - "JRE found with `which java`!"); - // remove suf - *(line+(line_size-suf_size)) = '\0'; - found = GetJREPathFromBase(path, pathSize, line, arch); - } - } - } - pclose(cmd_fp); - - return found; -} - -/* - * Find path to JRE based on .exe's location or registry settings. - */ -bool GetJREPathOSSpecific(char* path, size_t pathSize, const char* arch) -{ - bool found = false; - - // check if a JRE is located in a common location - if (!found) { - found = GetJREPathInCommonLocations(path, pathSize, arch); - } - - // check if a JRE is located in a common location - if (!found) { - found = GetJREPathWhichJava(path, pathSize, arch); - } - - return found; -} - -#endif // !defined _WIN32 diff --git a/AI/Interfaces/Java/src/main/native/JvmLocater_windows.c b/AI/Interfaces/Java/src/main/native/JvmLocater_windows.c deleted file mode 100644 index 6bb5367f2b6..00000000000 --- a/AI/Interfaces/Java/src/main/native/JvmLocater_windows.c +++ /dev/null @@ -1,181 +0,0 @@ -/* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ - -#if defined _WIN32 - -#include "JvmLocater_common.h" - -#include "System/MainDefines.h" -#include "System/SafeCStrings.h" - -#include -#include -#include - - -#define JVM_LIB "jvm.dll" - -#define JAVA_LIB "java.dll" - -/* - * Returns the arch path, to get the current arch use the - * macro GetArch, nbits here is ignored for now. - */ -const char* GetArchPath() -{ -#ifdef _M_AMD64 - return "amd64"; -#elif defined(_M_IA64) - return "ia64"; -#else - return "i386"; -#endif -} - -/* - * Given a JRE location and a JVM type, construct what the name the - * JVM shared library will be. Return true, if such a library - * exists, false otherwise. - */ -bool GetJVMPath(const char* jrePath, const char* jvmType, - char* jvmPath, size_t jvmPathSize, const char* arch) -{ - if (arch == NULL) { - arch = GetArchPath(); - } - - if ((*jvmType == '/') || (*jvmType == '\\')) { - SNPRINTF(jvmPath, jvmPathSize, "%s\\"JVM_LIB, jvmType); - } else { - SNPRINTF(jvmPath, jvmPathSize, "%s\\bin\\%s\\"JVM_LIB, jrePath, - jvmType); - } - - return FileExists(jvmPath); -} - -static bool CheckIfJREPath(const char* path, const char* arch) -{ - bool found = false; - - if (path != NULL) { - char libJava[MAXPATHLEN]; - - // Is path a JRE path? - SNPRINTF(libJava, MAXPATHLEN, "%s\\bin\\"JAVA_LIB, path); - if (FileExists(libJava)) { - found = true; - } - } - - return found; -} - -bool GetJREPathFromBase(char* path, size_t pathSize, const char* basePath, - const char* arch) -{ - bool found = false; - - if (basePath != NULL) { - //if (GetApplicationHome(path, pathSize)) { - char jrePath[MAXPATHLEN]; - - // Is basePath a JRE path? - STRCPY_T(jrePath, MAXPATHLEN, basePath); - if (CheckIfJREPath(jrePath, arch)) { - STRCPY_T(path, pathSize, basePath); - found = true; - } - - // Is basePath/jre a JRE path? - STRCAT_T(jrePath, MAXPATHLEN, "\\jre"); - if (CheckIfJREPath(jrePath, arch)) { - STRCPY_T(path, pathSize, jrePath); - found = true; - } - } - - return found; -} - -/* - * Helpers to look in the registry for a public JRE. - */ - -/* Same for 1.5.0, 1.5.1, 1.5.2 etc. */ -#define JRE_REG_KEY "Software\\JavaSoft\\Java Runtime Environment" - -static bool GetStringFromRegistry(HKEY key, const char* name, char* buf, - size_t bufSize) -{ - DWORD type, size; - - if (RegQueryValueEx(key, name, 0, &type, 0, &size) == 0 - && type == REG_SZ - && (size < bufSize)) { - if (RegQueryValueEx(key, name, 0, 0, (LPBYTE)buf, &size) == 0) { - return true; - } - } - return false; -} - -static bool GetJREPathFromRegistry(char* path, size_t pathSize, const char* arch) -{ - HKEY key, subkey; - char version[MAXPATHLEN]; - - /* - * Note: There is a very similar implementation of the following - * registry reading code in the Windows java control panel (javacp.cpl). - * If there are bugs here, a similar bug probably exists there. Hence, - * changes here require inspection there. - */ - - // Find the current version of the JRE - if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, JRE_REG_KEY, 0, KEY_READ, &key) != 0) { - return false; - } - - if (!GetStringFromRegistry(key, "CurrentVersion", - version, sizeof(version))) { - RegCloseKey(key); - return false; - } - - /*if (strcmp(version, wantedVersion) != 0) { - RegCloseKey(key); - return false; - }*/ - - // Find directory where the current version is installed. - if (RegOpenKeyEx(key, version, 0, KEY_READ, &subkey) != 0) { - RegCloseKey(key); - return false; - } - - if (!GetStringFromRegistry(subkey, "JavaHome", path, pathSize)) { - RegCloseKey(key); - RegCloseKey(subkey); - return false; - } - - RegCloseKey(key); - RegCloseKey(subkey); - - simpleLog_logL(LOG_LEVEL_NOTICE, "JRE found in registry!"); - return true; -} - -bool GetJREPathOSSpecific(char* path, size_t pathSize, const char* arch) -{ - bool found = false; - - // check if a JRE is specified in the registry - if (!found) { - found = GetJREPathFromRegistry(path, pathSize, arch); - } - - return found; -} - -#endif // defined _WIN32 diff --git a/AI/Skirmish/NullJavaAI/CMakeLists.txt b/AI/Skirmish/NullJavaAI/CMakeLists.txt deleted file mode 100644 index ffac49c05b4..00000000000 --- a/AI/Skirmish/NullJavaAI/CMakeLists.txt +++ /dev/null @@ -1,4 +0,0 @@ -### Generic Java Skirmish AI config -# - -configure_java_skirmish_ai("") diff --git a/AI/Skirmish/NullJavaAI/VERSION b/AI/Skirmish/NullJavaAI/VERSION deleted file mode 100644 index ceab6e11ece..00000000000 --- a/AI/Skirmish/NullJavaAI/VERSION +++ /dev/null @@ -1 +0,0 @@ -0.1 \ No newline at end of file diff --git a/AI/Skirmish/NullJavaAI/bin/ant.dependent.properties b/AI/Skirmish/NullJavaAI/bin/ant.dependent.properties deleted file mode 100644 index 559cacc4e54..00000000000 --- a/AI/Skirmish/NullJavaAI/bin/ant.dependent.properties +++ /dev/null @@ -1,36 +0,0 @@ -# properties file for spring Java Skirmish AIs -# This is used when you do not use the spring repository for compiling. -# -# Paths are relative to the project home (which is ../ from this file). -# All values are optional, and listed here with their defaults. -# -# The default assumes that you have the spring source, -# and do a spring in-source build as it is the default with CMake and SCons, -# and as the buildbot does it. -# -# build-dir: ${spring.home}/AI/Skirmish/${ai.name}/ -# install-dir: ${spring.home}/dist/AI/Skirmish/${ai.name}/${ai.version}/ -# -# This file is loaded from within build.xml, -# but only if no "jlibs-interface/" dir is present. -# - -;spring.home=../../.. - -# This is used only in the next property -;build.home=${spring.home} -# Where jar files will be built -;build.dir=${build.home}/AI/Skirmish/${skirmish.ai.name} - -# This is used only in the next property -;dist.home=${spring.home}/dist -# Where jar files will be installed to -;dist.dir=${dist.home}/AI/Skirmish/${skirmish.ai.name}/${skirmish.ai.version} -# Where HTML JavaDoc files will be generated -;doc.dir=${dist.home}/AI/Skirmish/${skirmish.ai.name}/${skirmish.ai.version}/doc/jdoc - -# The following two are needed for compiling (to create the classpath) -# * Here we look for ./jlib/*.jar recursively -;ai.interface.src.home=${spring.home}/AI/Interfaces/Java -# * Here we look for AIInterface.jar -;ai.interface.build.home=${build.home}/AI/Interfaces/Java diff --git a/AI/Skirmish/NullJavaAI/bin/ant.independent.properties b/AI/Skirmish/NullJavaAI/bin/ant.independent.properties deleted file mode 100644 index 129a1589a21..00000000000 --- a/AI/Skirmish/NullJavaAI/bin/ant.independent.properties +++ /dev/null @@ -1,29 +0,0 @@ -# properties file for spring Java Skirmish AIs -# This is used when you do not use the spring repository for compiling. -# -# Paths are relative to the project home (which is ../ from this file). -# All values are optional, and listed here with their defaults. -# -# For the project, you do not need the spring sources, -# but you will need the "jlibs-interface/" instead, which contains -# the Java AI Interfaces jlibs plus its binary and source jar. -# -# build-dir: build/ -# install-dir: dist/ -# -# This file is loaded from within build.xml, -# but only if a "jlibs-interface/" dir is present. -# - -# This is used only in the next property -;build.home=build -# Where jar files will be built -;build.dir=${build.home} - -# This is used only in the next property. -# You will want to set dist.home to your spring install dir. -;dist.home=dist -# Where jar files will be installed to -;dist.dir=${dist.home}/AI/Skirmish/${skirmish.ai.name}/${skirmish.ai.version} -# Where HTML JavaDoc files will be generated -;doc.dir=${dist.home}/AI/Skirmish/${skirmish.ai.name}/${skirmish.ai.version}/doc/jdoc diff --git a/AI/Skirmish/NullJavaAI/bin/build.xml b/AI/Skirmish/NullJavaAI/bin/build.xml deleted file mode 100644 index 8d3b185b6bf..00000000000 --- a/AI/Skirmish/NullJavaAI/bin/build.xml +++ /dev/null @@ -1,279 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - . -File not found: ${ai.interface.jar} -Please make sure to compile the Java AI Interface -before trying to compile this AI, or request -an independent version of this project from -wherever you got this AI from. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Successfully bumped ${skirmish.ai.name} version from ${skirmish.ai.version} to ${skirmish.ai.version.new}. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - . -Please specify either home.new or name.new, for example: - ant create-independent -Dhome.new=~/projects/SpringSkirmishAIs/ -or - ant create-independent -Dname.new=MyJavaAI - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Spring source independant Java Skirmish AI project copied to: - ${project.dir.new} - -You may want to change your java source package structure -and the data/AIInto.lua file for the new project. - - - - - - - - - - - - - - - - - diff --git a/AI/Skirmish/NullJavaAI/data/AIInfo.lua b/AI/Skirmish/NullJavaAI/data/AIInfo.lua deleted file mode 100644 index 75f19a8632b..00000000000 --- a/AI/Skirmish/NullJavaAI/data/AIInfo.lua +++ /dev/null @@ -1,53 +0,0 @@ --- --- Info Definition Table format --- --- --- These keywords must be lowercase for LuaParser to read them. --- --- key: user defined or one of the SKIRMISH_AI_PROPERTY_* defines in --- SSkirmishAILibrary.h --- value: the value of the property --- desc: the description (could be used as a tooltip) --- --- --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- - -local infos = { - { - key = 'shortName', - value = 'NullJavaAI', - desc = 'machine conform name.', - }, - { - key = 'version', - value = '0.1', -- AI version - !This comment is used for parsing! - }, - { - key = 'className', - value = 'nulljavaai.NullJavaAI', - desc = 'fully qualified name of a class that implements interface com.springrts.ai.AI', - }, - { - key = 'name', - value = 'low-level Java test Skirmish AI', - desc = 'human readable name.', - }, - { - key = 'loadSupported', - value = 'no', - desc = 'whether this AI supports loading or not', - }, - { - key = 'interfaceShortName', - value = 'Java', -- AI Interface name - !This comment is used for parsing! - desc = 'the shortName of the AI interface this AI needs', - }, - { - key = 'interfaceVersion', - value = '0.1', -- AI Interface version - !This comment is used for parsing! - desc = 'the minimum version of the AI interface required by this AI', - }, -} - -return infos diff --git a/AI/Skirmish/NullJavaAI/data/AIOptions.lua b/AI/Skirmish/NullJavaAI/data/AIOptions.lua deleted file mode 100644 index d435d62e964..00000000000 --- a/AI/Skirmish/NullJavaAI/data/AIOptions.lua +++ /dev/null @@ -1,15 +0,0 @@ --- --- Custom Options Definition Table format --- --- A detailed example of how this format works can be found --- in the spring source under: --- AI/Skirmish/NullAI/data/AIOptions.lua --- --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- - -local options = { -} - -return options - diff --git a/AI/Skirmish/NullJavaAI/manifest.mf b/AI/Skirmish/NullJavaAI/manifest.mf deleted file mode 100755 index 59499bce4a2..00000000000 --- a/AI/Skirmish/NullJavaAI/manifest.mf +++ /dev/null @@ -1,2 +0,0 @@ -Manifest-Version: 1.0 - diff --git a/AI/Skirmish/NullJavaAI/src/nulljavaai/NullJavaAI.java b/AI/Skirmish/NullJavaAI/src/nulljavaai/NullJavaAI.java deleted file mode 100644 index 41c42c6c235..00000000000 --- a/AI/Skirmish/NullJavaAI/src/nulljavaai/NullJavaAI.java +++ /dev/null @@ -1,187 +0,0 @@ -/* - Copyright (c) 2008 Robin Vobruba - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -*/ - -package nulljavaai; - - -import com.springrts.ai.*; - -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.logging.*; - -/** - * Serves as Interface for a Java Skirmish AIs for the Spring engine. - * - * @author hoijui - * @version 0.1 - */ -public class NullJavaAI extends AbstractAI implements AI { - - private int skirmishAIId = -1; - private AICallback clb = null; - private String myLogFile = null; - private Logger log = null; - private int frame = -1; - - private static class MyCustomLogFormatter extends Formatter { - - private DateFormat dateFormat = new SimpleDateFormat("HH:mm:ss:SSS dd.MM.yyyy"); - - public String format(LogRecord record) { - - // Create a StringBuffer to contain the formatted record - // start with the date. - StringBuffer sb = new StringBuffer(); - - // Get the date from the LogRecord and add it to the buffer - Date date = new Date(record.getMillis()); - sb.append(dateFormat.format(date)); - sb.append(" "); - - // Get the level name and add it to the buffer - sb.append(record.getLevel().getName()); - sb.append(": "); - - // Get the formatted message (includes localization - // and substitution of parameters) and add it to the buffer - sb.append(formatMessage(record)); - sb.append("\n"); - - return sb.toString(); - } - } - - public static boolean isDebugging() { - return true; - } - - public NullJavaAI() {} - - - @Override - public int init(int skirmishAIId, AICallback callback) { - - int ret = -1; - - this.clb = callback; - - // initialize the log - try { - int teamId = clb.SkirmishAI_getTeamId(); - myLogFile = callback.DataDirs_locatePath("log-team-" + teamId + "-" + skirmishAIId + ".txt", true, true, false, false); - FileHandler fileLogger = new FileHandler(myLogFile, false); - fileLogger.setFormatter(new MyCustomLogFormatter()); - fileLogger.setLevel(Level.ALL); - log = Logger.getLogger("nulljavaai"); - if (isDebugging()) { - log.setLevel(Level.ALL); - } else { - log.setLevel(Level.INFO); - } - log.addHandler(fileLogger); - } catch (Exception ex) { - System.out.println("NullJavaAI: Failed initializing the logger!"); - ex.printStackTrace(); - ret = -2; - } - - try { - int teamId = clb.SkirmishAI_getTeamId(); - log.info("initializing team " + teamId + ", id " + skirmishAIId); - - int numInfo = callback.SkirmishAI_Info_getSize(); - log.info("info (items: " + numInfo + ") ..."); - for (int i = 0; i < numInfo; i++) { - String key = callback.SkirmishAI_Info_getKey(i); - String value = callback.SkirmishAI_Info_getValue(i); - log.info(key + " = " + value); - } - - int numOptions = callback.SkirmishAI_OptionValues_getSize(); - log.info("options (items: " + numOptions + ") ..."); - for (int i = 0; i < numOptions; i++) { - String key = callback.SkirmishAI_OptionValues_getKey(i); - String value = callback.SkirmishAI_OptionValues_getValue(i); - log.info(key + " = " + value); - } - - ret = 0; - } catch (Exception ex) { - log.log(Level.SEVERE, "Failed initializing", ex); - log.log(Level.SEVERE, "msg: " + ex.getMessage()); - StackTraceElement[] stackTrace = ex.getStackTrace(); - for (int i = 0; i < stackTrace.length; i++) { - log.log(Level.SEVERE, "ste: " + stackTrace[i].toString()); - } - ret = -3; - } - - { - int numOptions = clb.SkirmishAI_OptionValues_getSize(); - log.info("sizeOptions: " + numOptions); - log.info("options:"); -// Pointer[] pKeys = evt.optionKeys.getPointerArray(0L, evt.sizeOptions); -// Pointer[] pValues = evt.optionValues.getPointerArray(0L, evt.sizeOptions); -// Properties options = new Properties(); -// for (int i = 0; i < evt.sizeOptions; i++) { -// options.setProperty(pKeys[i].getString(0L), pValues[i].getString(0L)); -// } - for (int i = 0; i < numOptions; i++) { - String key = clb.SkirmishAI_OptionValues_getKey(i); - String value = clb.SkirmishAI_OptionValues_getValue(i); - log.info(key + " = " + value); - } - log.info("stored"); - } - - return ret; - } - - @Override - public int update(int frame) { - - try { - log.finer("update event ..."); - log.finer("frame: " + frame); - } catch (Exception ex) { - log.log(Level.WARNING, "Failed handling event", ex); - return -1; - } - - return 0; - } - - @Override - public int release(int reason) { - - int ret = -1; - - try { - int teamId = clb.SkirmishAI_getTeamId(); - log.info("releasing team " + teamId + ", id " + skirmishAIId); - - ret = 0; - } catch (Exception ex) { - log.log(Level.WARNING, "Failed releasing", ex); - ret = -2; - } - - return ret; - } -} diff --git a/AI/Skirmish/NullOOJavaAI/CMakeLists.txt b/AI/Skirmish/NullOOJavaAI/CMakeLists.txt deleted file mode 100644 index 195bb8f6313..00000000000 --- a/AI/Skirmish/NullOOJavaAI/CMakeLists.txt +++ /dev/null @@ -1,4 +0,0 @@ -### Generic Java Skirmish AI config -# - -configure_java_skirmish_ai("JavaOO") diff --git a/AI/Skirmish/NullOOJavaAI/VERSION b/AI/Skirmish/NullOOJavaAI/VERSION deleted file mode 100644 index ceab6e11ece..00000000000 --- a/AI/Skirmish/NullOOJavaAI/VERSION +++ /dev/null @@ -1 +0,0 @@ -0.1 \ No newline at end of file diff --git a/AI/Skirmish/NullOOJavaAI/bin/ant.dependent.properties b/AI/Skirmish/NullOOJavaAI/bin/ant.dependent.properties deleted file mode 100644 index 71674185fe3..00000000000 --- a/AI/Skirmish/NullOOJavaAI/bin/ant.dependent.properties +++ /dev/null @@ -1,40 +0,0 @@ -# properties file for spring Java Skirmish AIs -# This is used when you do not use the spring repository for compiling. -# -# Paths are relative to the project home (which is ../ from this file). -# All values are optional, and listed here with their defaults. -# -# The default assumes that you have the spring source, -# and do a spring in-source build as it is the default with CMake and SCons, -# and as the buildbot does it. -# -# build-dir: ${spring.home}/AI/Skirmish/${ai.name}/ -# install-dir: ${spring.home}/dist/AI/Skirmish/${ai.name}/${ai.version}/ -# -# This file is loaded from within build.xml, -# but only if "Java-AIInterface/" and "JavaOO-AIWrapper/" dirs are not present. -# - -;spring.home=../../.. - -# This is used only in the next property -;build.home=${spring.home} -# Where jar files will be built -;build.dir=${build.home}/AI/Skirmish/${skirmish.ai.name} - -# This is used only in the next property -;dist.home=${spring.home}/dist -# Where jar files will be installed to -;dist.dir=${dist.home}/AI/Skirmish/${skirmish.ai.name}/${skirmish.ai.version} -# Where HTML JavaDoc files will be generated -;doc.dir=${dist.home}/AI/Skirmish/${skirmish.ai.name}/${skirmish.ai.version}/doc/jdoc - -# The following four are needed for compiling (to create the classpath) -# * Here we look for ./data/jlib/*.jar recursively -;ai.interface.src.home=${spring.home}/AI/Interfaces/Java -# * Here we look for AIInterface.jar -;ai.interface.build.home=${build.home}/AI/Interfaces/Java -# * Here we look for ./jlib/*.jar recursively -;ai.wrapper.oo.src.home=${spring.home}/AI/Wrappers/JavaOO -# * Here we look for JavaOO-AIWrapper.jar -;ai.wrapper.oo.build.home=${build.home}/AI/Wrappers/JavaOO diff --git a/AI/Skirmish/NullOOJavaAI/bin/ant.independent.properties b/AI/Skirmish/NullOOJavaAI/bin/ant.independent.properties deleted file mode 100644 index 4f5648f2935..00000000000 --- a/AI/Skirmish/NullOOJavaAI/bin/ant.independent.properties +++ /dev/null @@ -1,30 +0,0 @@ -# properties file for spring Java Skirmish AIs -# This is used when you do not use the spring repository for compiling. -# -# Paths are relative to the project home (which is ../ from this file). -# All values are optional, and listed here with their defaults. -# -# For the project, you do not need the spring sources, -# but you will need the "Java-AIInterface/" and "JavaOO-AIWrapper/" dirs -# instead, which contain the Java AI Interfaces jlibs plus its binary -# and source jar, and the Java OO Wrappers equal counterparts. -# -# build-dir: build/ -# install-dir: dist/ -# -# This file is loaded from within build.xml, -# but only if "Java-AIInterface/" and "JavaOO-AIWrapper/" dirs are present. -# - -# This is used only in the next property -;build.home=build -# Where jar files will be built -;build.dir=${build.home} - -# This is used only in the next property. -# You will want to set dist.home to your spring install dir. -;dist.home=dist -# Where jar files will be installed to -;dist.dir=${dist.home}/AI/Skirmish/${skirmish.ai.name}/${skirmish.ai.version} -# Where HTML JavaDoc files will be generated -;doc.dir=${dist.home}/AI/Skirmish/${skirmish.ai.name}/${skirmish.ai.version}/doc/jdoc diff --git a/AI/Skirmish/NullOOJavaAI/bin/build.xml b/AI/Skirmish/NullOOJavaAI/bin/build.xml deleted file mode 100644 index 32b0b4783eb..00000000000 --- a/AI/Skirmish/NullOOJavaAI/bin/build.xml +++ /dev/null @@ -1,342 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - . -File not found: ${ai.interface.build.home}/AIInterface.jar -Please make sure to compile the Java AI Interface -before trying to compile this AI, or request -an independent version of this project from -wherever you got this AI. - - - - . -File not found: ${ai.wrapper.oo.build.home}/JavaOO-AIWrapper.jar -Please make sure to compile the Java OO AI Wrapper -before trying to compile this AI, or request -an independent version of this project from -wherever you got this AI from. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Successfully bumped ${skirmish.ai.name} version from ${skirmish.ai.version} to ${skirmish.ai.version.new}. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - . -Please specify either home.new or name.new, for example: - ant create-independent -Dhome.new=~/projects/SpringSkirmishAIs/ -or - ant create-independent -Dname.new=MyJavaAI - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Spring source independant Java Skirmish AI project copied to: - ${project.dir.new} - -You may want to change your java source package structure -and the data/AIInto.lua file for the new project. - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/AI/Skirmish/NullOOJavaAI/data/AIInfo.lua b/AI/Skirmish/NullOOJavaAI/data/AIInfo.lua deleted file mode 100644 index 91420c5e704..00000000000 --- a/AI/Skirmish/NullOOJavaAI/data/AIInfo.lua +++ /dev/null @@ -1,53 +0,0 @@ --- --- Info Definition Table format --- --- --- These keywords must be lowercase for LuaParser to read them. --- --- key: user defined or one of the SKIRMISH_AI_PROPERTY_* defines in --- SSkirmishAILibrary.h --- value: the value of the property --- desc: the description (could be used as a tooltip) --- --- --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- - -local infos = { - { - key = 'shortName', - value = 'NullOOJavaAI', - desc = 'machine conform name.', - }, - { - key = 'version', - value = '0.1', -- AI version - !This comment is used for parsing! - }, - { - key = 'className', - value = 'nulloojavaai.NullOOJavaAI', - desc = 'fully qualified name of a class that implements interface com.springrts.ai.AI', - }, - { - key = 'name', - value = 'high-level Java stub Skirmish AI', - desc = 'human readable name.', - }, - { - key = 'loadSupported', - value = 'no', - desc = 'whether this AI supports loading or not', - }, - { - key = 'interfaceShortName', - value = 'Java', -- AI Interface name - !This comment is used for parsing! - desc = 'the shortName of the AI interface this AI needs', - }, - { - key = 'interfaceVersion', - value = '0.1', -- AI Interface version - !This comment is used for parsing! - desc = 'the minimum version of the AI interface required by this AI', - }, -} - -return infos diff --git a/AI/Skirmish/NullOOJavaAI/data/AIOptions.lua b/AI/Skirmish/NullOOJavaAI/data/AIOptions.lua deleted file mode 100644 index d435d62e964..00000000000 --- a/AI/Skirmish/NullOOJavaAI/data/AIOptions.lua +++ /dev/null @@ -1,15 +0,0 @@ --- --- Custom Options Definition Table format --- --- A detailed example of how this format works can be found --- in the spring source under: --- AI/Skirmish/NullAI/data/AIOptions.lua --- --------------------------------------------------------------------------------- --------------------------------------------------------------------------------- - -local options = { -} - -return options - diff --git a/AI/Skirmish/NullOOJavaAI/manifest.mf b/AI/Skirmish/NullOOJavaAI/manifest.mf deleted file mode 100755 index 59499bce4a2..00000000000 --- a/AI/Skirmish/NullOOJavaAI/manifest.mf +++ /dev/null @@ -1,2 +0,0 @@ -Manifest-Version: 1.0 - diff --git a/AI/Skirmish/NullOOJavaAI/src/main/java/nulloojavaai/NullOOJavaAI.java b/AI/Skirmish/NullOOJavaAI/src/main/java/nulloojavaai/NullOOJavaAI.java deleted file mode 100644 index 9ffabe1eae6..00000000000 --- a/AI/Skirmish/NullOOJavaAI/src/main/java/nulloojavaai/NullOOJavaAI.java +++ /dev/null @@ -1,320 +0,0 @@ -/* - Copyright (c) 2008 Robin Vobruba - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -*/ - -package nulloojavaai; - - -import com.springrts.ai.AI; -import com.springrts.ai.oo.AIFloat3; -import com.springrts.ai.oo.OOAI; -import com.springrts.ai.oo.clb.*; - -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Properties; -import java.util.List; -import java.util.logging.*; - -/** - * Serves as Interface for a Java Skirmish AIs for the Spring engine. - * - * @author hoijui - */ -public class NullOOJavaAI extends OOAI implements AI { - - private static class MyCustomLogFormatter extends Formatter { - - private DateFormat dateFormat = new SimpleDateFormat("HH:mm:ss:SSS dd.MM.yyyy"); - - public String format(LogRecord record) { - - // Create a StringBuffer to contain the formatted record - // start with the date. - StringBuffer sb = new StringBuffer(); - - // Get the date from the LogRecord and add it to the buffer - Date date = new Date(record.getMillis()); - sb.append(dateFormat.format(date)); - sb.append(" "); - - // Get the level name and add it to the buffer - sb.append(record.getLevel().getName()); - sb.append(": "); - - // Get the formatted message (includes localization - // and substitution of parameters) and add it to the buffer - sb.append(formatMessage(record)); - sb.append("\n"); - - return sb.toString(); - } - } - - private static void logProperties(Logger log, Level level, Properties props) { - - log.log(level, "properties (items: " + props.size() + "):"); - for (String key : props.stringPropertyNames()) { - log.log(level, key + " = " + props.getProperty(key)); - } - } - - private int skirmishAIId = -1; - private int teamId = -1; - private Properties info = null; - private Properties optionValues = null; - private OOAICallback clb = null; - private String myLogFile = null; - private Logger log = null; - - private static final int DEFAULT_ZONE = 0; - - - public NullOOJavaAI() {} - - private int sendTextMsg(String msg) { - - try { - clb.getGame().sendTextMessage("/say " + msg, DEFAULT_ZONE); - } catch (Exception ex) { - ex.printStackTrace(); - return 1; - } - - return 0; - } - public boolean isDebugging() { - return true; - } - - @Override - public int init(int skirmishAIId, OOAICallback callback) { - - int ret = -1; - - this.skirmishAIId = skirmishAIId; - this.clb = callback; - - this.teamId = clb.getSkirmishAI().getTeamId(); - - info = new Properties(); - Info inf = clb.getSkirmishAI().getInfo(); - int numInfo = inf.getSize(); - for (int i = 0; i < numInfo; i++) { - String key = inf.getKey(i); - String value = inf.getValue(i); - info.setProperty(key, value); - } - - optionValues = new Properties(); - OptionValues opVals = clb.getSkirmishAI().getOptionValues(); - int numOpVals = opVals.getSize(); - for (int i = 0; i < numOpVals; i++) { - String key = opVals.getKey(i); - String value = opVals.getValue(i); - optionValues.setProperty(key, value); - } - - // initialize the log - try { - myLogFile = callback.getDataDirs().locatePath("log-team-" + teamId + ".txt", true, true, false, false); - FileHandler fileLogger = new FileHandler(myLogFile, false); - fileLogger.setFormatter(new MyCustomLogFormatter()); - fileLogger.setLevel(Level.ALL); - log = Logger.getLogger("nulloojavaai"); - log.addHandler(fileLogger); - if (isDebugging()) { - log.setLevel(Level.ALL); - } else { - log.setLevel(Level.INFO); - } - } catch (Exception ex) { - System.out.println("NullOOJavaAI: Failed initializing the logger!"); - ex.printStackTrace(); - ret = -2; - } - - try { - log.info("initializing team " + teamId); - - log.log(Level.FINE, "info:"); - logProperties(log, Level.FINE, info); - - log.log(Level.FINE, "options:"); - logProperties(log, Level.FINE, optionValues); - - ret = 0; - } catch (Exception ex) { - log.log(Level.SEVERE, "Failed initializing", ex); - ret = -3; - } - - return ret; - } - - @Override - public int release(int reason) { - return 0; // signaling: OK - } - - @Override - public int update(int frame) { - - if (frame % 300 == 0) { - sendTextMsg("listing resources ..."); - List resources = clb.getResources(); - for (Resource resource : resources) { - sendTextMsg("Resource " + resource.getName() + " optimum: " - + resource.getOptimum()); - sendTextMsg("Resource " + resource.getName() + " current: " - + clb.getEconomy().getCurrent(resource)); - sendTextMsg("Resource " + resource.getName() + " income: " - + clb.getEconomy().getIncome(resource)); - sendTextMsg("Resource " + resource.getName() + " storage: " - + clb.getEconomy().getStorage(resource)); - sendTextMsg("Resource " + resource.getName() + " usage: " - + clb.getEconomy().getUsage(resource)); - } - } - - return 0; // signaling: OK - } - - @Override - public int message(int player, String message) { - return 0; // signaling: OK - } - - @Override - public int unitCreated(Unit unit, Unit builder) { - - int ret = sendTextMsg("unitCreated: " + unit.toString()); - sendTextMsg("unitCreated def: " + unit.getDef().getName()); - List buildOptions = unit.getDef().getBuildOptions(); - for (UnitDef unitDef : buildOptions) { - sendTextMsg("\tbuildOption x: " + unitDef.getName() + "\t" + unitDef.getHumanName() + "\t" + unitDef.toString() + "\t" + unitDef.hashCode()); - } - - return ret; - } - - @Override - public int unitFinished(Unit unit) { - return 0; // signaling: OK - } - - @Override - public int unitIdle(Unit unit) { - return 0; // signaling: OK - } - - @Override - public int unitMoveFailed(Unit unit) { - return 0; // signaling: OK - } - - @Override - public int unitDamaged(Unit unit, Unit attacker, float damage, AIFloat3 dir, WeaponDef weaponDef, boolean paralyzed) { - return 0; // signaling: OK - } - - @Override - public int unitDestroyed(Unit unit, Unit attacker) { - return 0; // signaling: OK - } - - @Override - public int unitGiven(Unit unit, int oldTeamId, int newTeamId) { - return 0; // signaling: OK - } - - @Override - public int unitCaptured(Unit unit, int oldTeamId, int newTeamId) { - return 0; // signaling: OK - } - - @Override - public int enemyEnterLOS(Unit enemy) { - return 0; // signaling: OK - } - - @Override - public int enemyLeaveLOS(Unit enemy) { - return 0; // signaling: OK - } - - @Override - public int enemyEnterRadar(Unit enemy) { - return 0; // signaling: OK - } - - @Override - public int enemyLeaveRadar(Unit enemy) { - return 0; // signaling: OK - } - - @Override - public int enemyDamaged(Unit enemy, Unit attacker, float damage, AIFloat3 dir, WeaponDef weaponDef, boolean paralyzed) { - return 0; // signaling: OK - } - - @Override - public int enemyDestroyed(Unit enemy, Unit attacker) { - return 0; // signaling: OK - } - - @Override - public int weaponFired(Unit unit, WeaponDef weaponDef) { - return 0; // signaling: OK - } - - @Override - public int playerCommand(java.util.List units, int commandTopicId, int playerId) { - return 0; // signaling: OK - } - - @Override - public int commandFinished(Unit unit, int commandId, int commandTopicId) { - return 0; // signaling: OK - } - - @Override - public int seismicPing(AIFloat3 pos, float strength) { - return 0; // signaling: OK - } - - @Override - public int load(String file) { - return 0; // signaling: OK - } - - @Override - public int save(String file) { - return 0; // signaling: OK - } - - @Override - public int enemyCreated(Unit enemy) { - return 0; // signaling: OK - } - - @Override - public int enemyFinished(Unit enemy) { - return 0; // signaling: OK - } - -} diff --git a/AI/Wrappers/JavaOO/CMakeLists.txt b/AI/Wrappers/JavaOO/CMakeLists.txt deleted file mode 100644 index da45a64c6e6..00000000000 --- a/AI/Wrappers/JavaOO/CMakeLists.txt +++ /dev/null @@ -1,379 +0,0 @@ -### Java OO AI Wrapper -# -# Global variables set in this file: -# * BUILD_JavaOO_AIWRAPPER -# - -#enable_language(Java) - -# includes rts/build/cmake/UtilJava.cmake -include(UtilJava) - - -set(myName "JavaOO") - - -# Check if the user wants to compile the wrapper -if ("${AI_TYPES}" STREQUAL "ALL" OR "${AI_TYPES}" STREQUAL "JAVA") - set(AIWRAPPERS_JAVA TRUE) -else ("${AI_TYPES}" STREQUAL "ALL" OR "${AI_TYPES}" STREQUAL "JAVA") - set(AIWRAPPERS_JAVA FALSE) -endif ("${AI_TYPES}" STREQUAL "ALL" OR "${AI_TYPES}" STREQUAL "JAVA") - - -# Check dependencies of the wrapper are met -if (AIWRAPPERS_JAVA AND BUILD_Java_AIINTERFACE AND AWK_FOUND AND NOT myName MATCHES "${AI_EXCLUDE_REGEX}") - set_global(BUILD_${myName}_AIWRAPPER TRUE) -else (AIWRAPPERS_JAVA AND BUILD_Java_AIINTERFACE AND AWK_FOUND AND NOT myName MATCHES "${AI_EXCLUDE_REGEX}") - set_global(BUILD_${myName}_AIWRAPPER FALSE) - message("warning: Java OO AI Wrapper will not be built!") -endif (AIWRAPPERS_JAVA AND BUILD_Java_AIINTERFACE AND AWK_FOUND AND NOT myName MATCHES "${AI_EXCLUDE_REGEX}") - - -# Build -if (BUILD_${myName}_AIWRAPPER) - set(myDir "${CMAKE_CURRENT_SOURCE_DIR}") - get_last_path_part(dirName ${myDir}) - set(myName "${dirName}") - set(myTarget "${myName}-AIWrapper") - set(myGenTarget "${myTarget}-generateSources") - set(myJavaTarget "${myTarget}-java") - set(myPomTarget "${myTarget}-pom") - set(mySourceDirRel "src/main/java") - make_absolute(mySourceDir "${myDir}" "${mySourceDirRel}") - - ai_wrapper_message(STATUS "Found AI Wrapper: ${myName}") - - - # Build library - set(myPkgFirstPart "com") - set(myParentPkg "${myPkgFirstPart}/springrts/ai") - set(myPkg "${myParentPkg}/oo") - set(myBinDir "${myDir}/bin") - set(commonAwkScriptsDir "${CMAKE_SOURCE_DIR}/AI/Wrappers/CUtils/bin") - set(myBuildDir "${CMAKE_CURRENT_BINARY_DIR}") - set(myGeneratedSourceDir "${myBuildDir}/src-generated/main/java") - set(myJavaBuildDir "${myBuildDir}/classes") - set(myJarFile "${myName}-AIWrapper") - set(myBinJarFile "${myJarFile}.jar") - set(mySrcJarFile "${myJarFile}-src.jar") - set(myJLibDir "${myDir}/jlib") - find_java_lib(vecmath_jar "vecmath" "${myJLibDir}") - set(myJLibs "${vecmath_jar}") - set(jAiIntJavaSourceDir "${JAVA_SRC_DIR_Java_AIINTERFACE}") - set(jAiIntJavaGeneratedSourceDir "${JAVA_GEN_SRC_DIR_Java_AIINTERFACE}") - set(myClassPath "${myJLibs}${PATH_DELIM_H}${CLASSPATH_Java_AIINTERFACE}") - - set_global(${myName}_AIWRAPPER_JAR_BIN "${myBuildDir}/${myBinJarFile}") - set_global(${myName}_AIWRAPPER_JAR_SRC "${myBuildDir}/${mySrcJarFile}") - set_global(${myName}_AIWRAPPER_JAR_CLASSPATH "${myJLibs}${PATH_DELIM_H}${${myName}_AIWRAPPER_JAR_BIN}") - set_global(${myName}_AIWRAPPER_TARGET "${myTarget}") - set_global(SOURCE_ROOT_${myName}_AIWRAPPER "${myDir}") - set_global(BUILD_ROOT_${myName}_AIWRAPPER "${CMAKE_CURRENT_BINARY_DIR}") - - # Locate the manifest file - find_manifest_file("${myDir}" myManifestFile) - if (myManifestFile) - set(myBinJarArgs "cmf" "${myManifestFile}") - else (myManifestFile) - set(myBinJarArgs "cf") - endif (myManifestFile) - - # remove all generated sources from build dir, if it exists - # (required for build dirs of git:master from before 21. September 2010) - file(REMOVE_RECURSE "${myGeneratedSourceDir}/${myPkg}") - - - # Generate sources - # ---------------- - - set(commonAwkScriptArgs - "-v" "INTERFACE_SOURCE_DIR=${jAiIntJavaSourceDir}" - "-v" "INTERFACE_GENERATED_SOURCE_DIR=${jAiIntJavaGeneratedSourceDir}" - "-v" "JAVA_GENERATED_SOURCE_DIR=${myGeneratedSourceDir}" - "-f" "${commonAwkScriptsDir}/common.awk" - "-f" "${commonAwkScriptsDir}/commonDoc.awk" - ) - - set(mySources - "${mySourceDir}/${myPkg}/AIEvent.java" - "${mySourceDir}/${myPkg}/AIFloat3.java" - "${mySourceDir}/${myPkg}/AIException.java" - "${mySourceDir}/${myPkg}/CallbackAIException.java" - "${mySourceDir}/${myPkg}/EventAIException.java" - ) - - set(myGenClbClasses - "Cheats" - "CommandDescription" - "Command" - "Damage" - "DataDirs" - "Debug" - "Drawer" - "Economy" - "Engine" - "FeatureDef" - "Feature" - "Figure" - "FlankingBonus" - "Game" - "GraphDrawer" - "GraphLine" - "Group" - "Info" - "Line" - "Log" - "Lua" - "Map" - "Mod" - "MoveData" - "OOAICallback" - "OptionValues" - "OrderPreview" - "Pathing" - "PathDrawer" - "Point" - "Resource" - "Roots" - "Shield" - "SkirmishAI" - "Team" - "OverlayTexture" - "UnitDef" - "Unit" - "Version" - "WeaponDef" - "WeaponMount" - "Weapon" - ) - set(myGeneratedCallbackSources ) - foreach (className ${myGenClbClasses}) - list(APPEND myGeneratedCallbackSources - "${myGeneratedSourceDir}/${myPkg}/clb/${className}.java" - "${myGeneratedSourceDir}/${myPkg}/clb/Abstract${className}.java" - "${myGeneratedSourceDir}/${myPkg}/clb/Stub${className}.java" - ) - if ("${className}" STREQUAL "CommandDescription") - list(APPEND myGeneratedCallbackSources - "${myGeneratedSourceDir}/${myPkg}/clb/WrappGroupSupportedCommand.java" - "${myGeneratedSourceDir}/${myPkg}/clb/WrappUnitSupportedCommand.java" - ) - elseif ("${className}" STREQUAL "Command") - list(APPEND myGeneratedCallbackSources - "${myGeneratedSourceDir}/${myPkg}/clb/WrappCurrentCommand.java" - ) - else () - list(APPEND myGeneratedCallbackSources - "${myGeneratedSourceDir}/${myPkg}/clb/Wrapp${className}.java" - ) - endif () - endforeach (className) - - set(myGeneratedEventSources - "${myGeneratedSourceDir}/${myPkg}/AbstractOOAI.java" - "${myGeneratedSourceDir}/${myPkg}/IOOAI.java" - "${myGeneratedSourceDir}/${myPkg}/OOAI.java" - "${myGeneratedSourceDir}/${myPkg}/IOOEventAI.java" - "${myGeneratedSourceDir}/${myPkg}/OOEventAI.java" - "${myGeneratedSourceDir}/${myPkg}/evt/UnitAIEvent.java" - "${myGeneratedSourceDir}/${myPkg}/evt/UnitLifeStateAIEvent.java" - "${myGeneratedSourceDir}/${myPkg}/evt/UnitTeamChangeAIEvent.java" - "${myGeneratedSourceDir}/${myPkg}/evt/EnemyAIEvent.java" - "${myGeneratedSourceDir}/${myPkg}/evt/LoadSaveAIEvent.java" - "${myGeneratedSourceDir}/${myPkg}/evt/CommandFinishedAIEvent.java" - "${myGeneratedSourceDir}/${myPkg}/evt/EnemyCreatedAIEvent.java" - "${myGeneratedSourceDir}/${myPkg}/evt/EnemyDamagedAIEvent.java" - "${myGeneratedSourceDir}/${myPkg}/evt/EnemyDestroyedAIEvent.java" - "${myGeneratedSourceDir}/${myPkg}/evt/EnemyFinishedAIEvent.java" - "${myGeneratedSourceDir}/${myPkg}/evt/EnemyEnterLOSAIEvent.java" - "${myGeneratedSourceDir}/${myPkg}/evt/EnemyEnterRadarAIEvent.java" - "${myGeneratedSourceDir}/${myPkg}/evt/EnemyLeaveLOSAIEvent.java" - "${myGeneratedSourceDir}/${myPkg}/evt/EnemyLeaveRadarAIEvent.java" - "${myGeneratedSourceDir}/${myPkg}/evt/InitAIEvent.java" - "${myGeneratedSourceDir}/${myPkg}/evt/LoadAIEvent.java" - "${myGeneratedSourceDir}/${myPkg}/evt/MessageAIEvent.java" - "${myGeneratedSourceDir}/${myPkg}/evt/LuaMessageAIEvent.java" - "${myGeneratedSourceDir}/${myPkg}/evt/PlayerCommandAIEvent.java" - "${myGeneratedSourceDir}/${myPkg}/evt/ReleaseAIEvent.java" - "${myGeneratedSourceDir}/${myPkg}/evt/SaveAIEvent.java" - "${myGeneratedSourceDir}/${myPkg}/evt/SeismicPingAIEvent.java" - "${myGeneratedSourceDir}/${myPkg}/evt/UnitCapturedAIEvent.java" - "${myGeneratedSourceDir}/${myPkg}/evt/UnitCreatedAIEvent.java" - "${myGeneratedSourceDir}/${myPkg}/evt/UnitDamagedAIEvent.java" - "${myGeneratedSourceDir}/${myPkg}/evt/UnitDestroyedAIEvent.java" - "${myGeneratedSourceDir}/${myPkg}/evt/UnitFinishedAIEvent.java" - "${myGeneratedSourceDir}/${myPkg}/evt/UnitGivenAIEvent.java" - "${myGeneratedSourceDir}/${myPkg}/evt/UnitIdleAIEvent.java" - "${myGeneratedSourceDir}/${myPkg}/evt/UnitMoveFailedAIEvent.java" - "${myGeneratedSourceDir}/${myPkg}/evt/UpdateAIEvent.java" - "${myGeneratedSourceDir}/${myPkg}/evt/WeaponFiredAIEvent.java" - ) - - set(myGeneratedSources - ${myGeneratedCallbackSources} - ${myGeneratedEventSources} - ) - set_source_files_properties(${myGeneratedSources} PROPERTIES GENERATED TRUE) - - set(jAiIntJavaGeneratedSources - "${jAiIntJavaGeneratedSourceDir}/${myParentPkg}/AI.java" - "${jAiIntJavaGeneratedSourceDir}/${myParentPkg}/AICallback.java" - ) - set_source_files_properties(${jAiIntJavaGeneratedSources} PROPERTIES GENERATED TRUE) - - - # Assemble project generated targets (and their libraries) we depend on - set(myDependTargets "${Java_AIINTERFACE_TARGET}") - set(myDependLibFiles "${Java_AIINTERFACE_JAR_BIN}") - set_source_files_properties(${myDependLibFiles} PROPERTIES GENERATED TRUE) - - add_custom_target(${myJavaTarget} - DEPENDS - "${myBuildDir}/${myBinJarFile}" - "${myBuildDir}/${mySrcJarFile}") - add_dependencies(${myJavaTarget} ${myDependTargets}) - - - # generate sources: callback - add_custom_command( - OUTPUT - ${myGeneratedCallbackSources} - COMMAND "${CMAKE_COMMAND}" - "-E" "remove_directory" "${myGeneratedSourceDir}/${myPkg}/clb" - COMMAND "${CMAKE_COMMAND}" - "-E" "make_directory" "${myGeneratedSourceDir}/${myPkg}/clb" - COMMAND "${AWK_BIN}" - ${commonAwkScriptArgs} - "-f" "${commonAwkScriptsDir}/commonOOCallback.awk" - "-f" "${myBinDir}/wrappCallback.awk" - "${jAiIntJavaGeneratedSourceDir}/${myParentPkg}/AICallback.java" - DEPENDS - "${commonAwkScriptsDir}/common.awk" - "${commonAwkScriptsDir}/commonDoc.awk" - "${commonAwkScriptsDir}/commonOOCallback.awk" - "${myBinDir}/wrappCallback.awk" - # Handled through a target level dependency, see below - #"${jAiIntJavaGeneratedSourceDir}/${myParentPkg}/AICallback.java" - WORKING_DIRECTORY - "${myBinDir}" - COMMENT - " ${myTarget}: Generating callback sources" VERBATIM - ) - - # generate sources: events - add_custom_command( - OUTPUT - ${myGeneratedEventSources} - COMMAND "${CMAKE_COMMAND}" - "-E" "remove_directory" "${myGeneratedSourceDir}/${myPkg}/evt" - COMMAND "${CMAKE_COMMAND}" - "-E" "make_directory" "${myGeneratedSourceDir}/${myPkg}/evt" - COMMAND "${AWK_BIN}" - ${commonAwkScriptArgs} - "-f" "${myBinDir}/wrappEvents.awk" - "${jAiIntJavaGeneratedSourceDir}/${myParentPkg}/AI.java" - DEPENDS - "${commonAwkScriptsDir}/common.awk" - "${commonAwkScriptsDir}/commonDoc.awk" - "${myBinDir}/wrappEvents.awk" - "${jAiIntJavaGeneratedSourceDir}/${myParentPkg}/AI.java" - WORKING_DIRECTORY - "${myBinDir}" - COMMENT - " ${myTarget}: Generating event sources" VERBATIM - ) - - add_custom_target(${myGenTarget} DEPENDS ${myGeneratedSources}) - add_dependencies(${myGenTarget} ${Java_AIINTERFACE_TARGET_GENERATE_SOURCES}) # for AICallback.java - add_dependencies(generateSources ${myGenTarget}) - - - # Build the jars - # -------------- - - # Write list of source files to an arg-file - set(mySrcArgFile "${CMAKE_CURRENT_BINARY_DIR}/sourceFiles.txt") - if (EXISTS "${mySrcArgFile}") - file(REMOVE "${mySrcArgFile}") - endif (EXISTS "${mySrcArgFile}") - foreach (srcFile ${mySources} ${myGeneratedSources}) - file(APPEND "${mySrcArgFile}" "\"${srcFile}\"\n") - endforeach (srcFile) - - # compile and pack library - add_custom_command( - OUTPUT - "${myBuildDir}/${myBinJarFile}" - COMMAND "${CMAKE_COMMAND}" - "-E" "remove_directory" "${myJavaBuildDir}" - COMMAND "${CMAKE_COMMAND}" - "-E" "make_directory" "${myJavaBuildDir}" - COMMAND "${Java_JAVAC_EXECUTABLE}" - "${JAVA_COMPILE_FLAG_CONDITIONAL}" - "-Xlint:deprecation" - "-cp" "${myClassPath}" - "-d" "${myJavaBuildDir}" - "@${mySrcArgFile}" - COMMAND "${Java_JAR_EXECUTABLE}" ARGS - ${myBinJarArgs} "${myBuildDir}/${myBinJarFile}" - "-C" "${myJavaBuildDir}" "${myPkgFirstPart}" - DEPENDS - ${myDependLibFiles} - ${mySources} - ${myGeneratedSources} - WORKING_DIRECTORY - "${myBinDir}" - COMMENT - " ${myTarget}: Compiling sources and packing library ${myBinJarFile}" VERBATIM - ) - - # pack sources - add_custom_command( - OUTPUT - "${myBuildDir}/${mySrcJarFile}" - COMMAND "${Java_JAR_EXECUTABLE}" ARGS - "cf" "${${myName}_AIWRAPPER_JAR_SRC}" - "-C" "${mySourceDir}" "${myPkgFirstPart}" - COMMAND "${Java_JAR_EXECUTABLE}" ARGS - "uf" "${${myName}_AIWRAPPER_JAR_SRC}" - "-C" "${myGeneratedSourceDir}" "${myPkgFirstPart}" - DEPENDS - ${myJavaSources} - ${myJavaGeneratedSources} - WORKING_DIRECTORY - "${myBuildDir}" - COMMENT - " ${myTarget}: Creating sources archive ${mySrcJarFile}" VERBATIM - ) - - # This sets the version in pom.xml - # as we have no separate version for the wrapper, - # we use the one from the interface - set(myMavenProperties "-Dmy.version=${Java_AIINTERFACE_VERS}") - add_custom_command( - OUTPUT - "${myBuildDir}/pom-generated.xml" - COMMAND "${CMAKE_COMMAND}" - "-Dfile.in=${myDir}/pom.xml" - "-Dfile.out=${myBuildDir}/pom-generated.xml" - ${myMavenProperties} - "-P" "${CMAKE_MODULES_SPRING}/ConfigureFile.cmake" - DEPENDS - "${myDir}/pom.xml" - WORKING_DIRECTORY - "${myDir}" - COMMENT - " ${myTarget}: Configure pom.xml" VERBATIM - ) - set_source_files_properties("${myBuildDir}/pom-generated.xml" PROPERTIES GENERATED TRUE) - - add_custom_target(${myPomTarget} - DEPENDS - "${myBuildDir}/pom-generated.xml") - - add_custom_target(${myTarget} ALL) - - add_dependencies(${myTarget} ${myJavaTarget}) - -endif (BUILD_${myName}_AIWRAPPER) diff --git a/AI/Wrappers/JavaOO/bin/ant.properties b/AI/Wrappers/JavaOO/bin/ant.properties deleted file mode 100644 index 0807d79b8a8..00000000000 --- a/AI/Wrappers/JavaOO/bin/ant.properties +++ /dev/null @@ -1,26 +0,0 @@ -# Paths are relative to the project home (which is ../ from this file). -# All values are optional. - -;spring.home=../../.. - -# specify Java-AIInterface dirs -;spring.ai.interface.src.home=${spring.home}/AI/Interfaces/Java -;spring.ai.interface.src.dir=${spring.ai.interface.src.home}/src/main/java -;spring.ai.interface.build.home=${build.home}/AI/Interfaces/Java -;spring.ai.interface.build.dir=${spring.ai.interface.build.home}/src-generated/main/java - -# This is used only in the next property -;build.home=${spring.home}/build -# Where jar files will be built -;build.dir=${build.home}/AI/Wrappers/${wrapper.name} - -# Where generated sources shall be created in -;src.generated=${build.dir}/src-generated/main -;src.generated.java=${src.generated}/java - -# This is used only in the next property -;dist.home=${spring.home}/dist -# Where jar files will be installed to -;dist.dir=${dist.home}/AI/Wrappers/${wrapper.name} -# Where HTML JavaDoc files will be generated -;doc.dir=${dist.home}/AI/Wrappers/${wrapper.name}/doc/jdoc diff --git a/AI/Wrappers/JavaOO/bin/build.xml b/AI/Wrappers/JavaOO/bin/build.xml deleted file mode 100644 index 71e836bbb7a..00000000000 --- a/AI/Wrappers/JavaOO/bin/build.xml +++ /dev/null @@ -1,211 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/AI/Wrappers/JavaOO/bin/wrappCallback.awk b/AI/Wrappers/JavaOO/bin/wrappCallback.awk deleted file mode 100755 index 7824eb30137..00000000000 --- a/AI/Wrappers/JavaOO/bin/wrappCallback.awk +++ /dev/null @@ -1,1241 +0,0 @@ -#!/usr/bin/awk -f -# -# This awk script creates Java classes in OO style to wrap the C style -# JNI based AI Callback wrapper. -# In other words, the output of this file wraps: -# com/springrts/ai/AICallback.java -# which wraps: -# rts/ExternalAI/Interface/SSkirmishAICallback.h -# and -# rts/ExternalAI/Interface/AISCommands.h -# -# This script uses functions from the following files: -# * common.awk -# * commonDoc.awk -# * commonOOCallback.awk -# Variables that can be set on the command-line (with -v): -# * GENERATED_SOURCE_DIR : the generated sources root dir -# * JAVA_GENERATED_SOURCE_DIR : the generated java sources root dir -# * INTERFACE_SOURCE_DIR : the Java AI Interfaces static source files root dir -# * INTERFACE_GENERATED_SOURCE_DIR : the Java AI Interfaces generated source files root dir -# -# usage: -# awk -f thisScript.awk -f common.awk -f commonDoc.awk -f commonOOCallback.awk -# awk -f thisScript.awk -f common.awk -f commonDoc.awk -f commonOOCallback.awk \ -# -v 'GENERATED_SOURCE_DIR=/tmp/build/AI/Interfaces/Java/src-generated' -# - -BEGIN { - # initialize things - - # define the field splitter(-regex) - FS = "(\\()|(\\);)"; - IGNORECASE = 0; - - # Used by other scripts - JAVA_MODE = 1; - - # These vars can be assigned externally, see file header. - # Set the default values if they were not supplied on the command line. - if (!GENERATED_SOURCE_DIR) { - GENERATED_SOURCE_DIR = "../src-generated/main"; - } - if (!JAVA_GENERATED_SOURCE_DIR) { - JAVA_GENERATED_SOURCE_DIR = GENERATED_SOURCE_DIR "/java"; - } - if (!INTERFACE_SOURCE_DIR) { - INTERFACE_SOURCE_DIR = "../../../Interfaces/Java/src/main/java"; - } - if (!INTERFACE_GENERATED_SOURCE_DIR) { - INTERFACE_GENERATED_SOURCE_DIR = "../../../Interfaces/Java/src-generated/main/java"; - } - - myMainPkgA = "com.springrts.ai"; - myParentPkgA = myMainPkgA ".oo"; - myPkgA = myParentPkgA ".clb"; - myPkgD = convertJavaNameFormAToD(myPkgA); - myClassVar = "ooClb"; - myWrapClass = "AICallback"; - myWrapVar = "innerCallback"; - - myBufferedClasses["UnitDef"] = 1; - myBufferedClasses["WeaponDef"] = 1; - myBufferedClasses["FeatureDef"] = 1; - - retParamName = "__retVal"; -} - -function createJavaFileName(clsName_c) { - return JAVA_GENERATED_SOURCE_DIR "/" myPkgD "/" clsName_c ".java"; -} - - -function printHeader(outFile_h, javaPkg_h, javaClassName_h, isInterface_h, - implementsInterface_h, isJniBound_h, isAbstract_h, implementsClass_h) { - - if (isInterface_h) { - classOrInterface_h = "interface"; - } else if (isAbstract_h) { - classOrInterface_h = "abstract class"; - } else { - classOrInterface_h = "class"; - } - - extensionsPart_h = ""; - if (isInterface_h) { - extensionsPart_h = " extends Comparable<" javaClassName_h ">"; - } else if (isAbstract_h) { - extensionsPart_h = " implements " implementsInterface_h ""; - } else { - extensionsPart_h = " extends " implementsClass_h " implements " implementsInterface_h; - } - - printCommentsHeader(outFile_h); - print("") >> outFile_h; - print("package " javaPkg_h ";") >> outFile_h; - print("") >> outFile_h; - print("") >> outFile_h; - print("import " myParentPkgA ".CallbackAIException;") >> outFile_h; - print("import " myParentPkgA ".AIFloat3;") >> outFile_h; - if (isJniBound_h) { - print("import " myMainPkgA ".AICallback;") >> outFile_h; - print("import " myMainPkgA ".Util;") >> outFile_h; - } - print("") >> outFile_h; - print("/**") >> outFile_h; - print(" * @author AWK wrapper script") >> outFile_h; - print(" * @version GENERATED") >> outFile_h; - print(" */") >> outFile_h; - print("public " classOrInterface_h " " javaClassName_h extensionsPart_h " {") >> outFile_h; - print("") >> outFile_h; -} - - -function getNullTypeValue(fRet_ntv) { - - if (fRet_ntv == "void") { - return ""; - } else if (fRet_ntv == "String") { - return "\"\""; - } else if (fRet_ntv == "AIFloat3") { - #return "new AIFloat3(0.0f, 0.0f, 0.0f)"; - return "null"; - } else if (fRet_ntv == "java.awt.Color") { - #return "java.awt.Color.BLACK"; - return "null"; - } else if (startsWithCapital(fRet_ntv)) { - # must be a class - return "null"; - } else if (match(fRet_ntv, /^java.util.List/)) { - return "null"; - } else if (match(fRet_ntv, /^java.util.Map/)) { - return "null"; - } else if (fRet_ntv == "boolean") { - return "false"; - } else { - return "0"; - } -} -function printTripleFunc(fRet_tr, fName_tr, fParams_tr, thrownExceptions_tr, outFile_int_tr, outFile_stb_tr, outFile_jni_tr, printIntAndStb_tr, noOverride_tr, isDeprecated_tr) { - - _funcHdr_tr = "public " fRet_tr " " fName_tr "(" fParams_tr ")"; - if (thrownExceptions_tr != "") { - _funcHdr_tr = _funcHdr_tr " throws " thrownExceptions_tr; - } - - if (printIntAndStb_tr) { - print("\t" _funcHdr_tr ";") >> outFile_int_tr; - print("") >> outFile_int_tr; - - isSimpleGetter_tr = ((fRet_tr != "void") && (fParams_tr == "") && match(fName_tr, /^(get|is)/)); - if ((fName_tr == "isEnabled") && match(outFile_int_tr, /Cheats\.java$/)) { - # an exception, because this has a setter anyway - isSimpleGetter_tr = 0; - } - nullTypeValue_tr = getNullTypeValue(fRet_tr); - if (isSimpleGetter_tr) { - # create an additional private member and setter - propName_tr = fName_tr; - sub(/^get/, "", propName_tr); - propName_tr = lowerize(propName_tr); - fSetterName_tr = fName_tr; - sub(/^(get|is)/, "set", fSetterName_tr); - print("\t" "public void " fSetterName_tr "(" fRet_tr " " propName_tr ")" " {") >> outFile_stb_tr; - print("\t\t" "this." propName_tr " = " propName_tr ";") >> outFile_stb_tr; - print("\t" "}") >> outFile_stb_tr; - print("\t" "private " fRet_tr " " propName_tr " = " nullTypeValue_tr ";") >> outFile_stb_tr; - if (isDeprecated_tr) { - # this prevents javac from outputting a warning - print("\t" "/** @deprecated */") >> outFile_stb_tr; - } - print("\t" "@Override") >> outFile_stb_tr; - print("\t" _funcHdr_tr " {") >> outFile_stb_tr; - print("\t\t" "return " propName_tr ";") >> outFile_stb_tr; - } else { - print("\t" "@Override") >> outFile_stb_tr; - print("\t" _funcHdr_tr " {") >> outFile_stb_tr; - # simply return a null like value - if (fRet_tr == "void") { - # return nothing - } else { - print("\t\t" "return " nullTypeValue_tr ";") >> outFile_stb_tr; - } - } - print("\t" "}") >> outFile_stb_tr; - print("") >> outFile_stb_tr; - } - - if (!noOverride_tr) { - print("\t" "@Override") >> outFile_jni_tr; - } - print("\t" _funcHdr_tr " {") >> outFile_jni_tr; - print("") >> outFile_jni_tr; -} - - -function printClasses() { - - # look for AVAILABLE indicators - for (_memberId in cls_memberId_metaComment) { - if (match(cls_memberId_metaComment[_memberId], /AVAILABLE:/)) { - _availCls = cls_memberId_metaComment[_memberId]; - sub(/^.*AVAILABLE:/, "", _availCls); - sub(/[ \t].*$/, "", _availCls); - clsAvailInd_memberId_cls[_memberId] = _availCls; - } - } - - c_size_cs = cls_id_name["*"]; - for (c=0; c < c_size_cs; c++) { - cls_cs = cls_id_name[c]; - anc_size_cs = cls_name_implIds[cls_cs ",*"]; - - printIntAndStb_cs = 1; - for (a=0; a < anc_size_cs; a++) { - implId_cs = cls_name_implIds[cls_cs "," a]; - printClass(implId_cs, cls_cs, printIntAndStb_cs); - # only print interface and stub when printing the first impl-class - printIntAndStb_cs = 0; - } - } -} - - -function printClass(implId_c, clsName_c, printIntAndStb_c) { - - implCls_c = implId_c; - sub(/^.*,/, "", implCls_c); - - clsName_int_c = clsName_c; - clsName_abs_c = "Abstract" clsName_int_c; - clsName_stb_c = "Stub" clsName_int_c; - _fullClsName = cls_implId_fullClsName[implId_c]; - if (_fullClsName != clsName_c) { - lastAncName_c = implId_c; - sub(/,[^,]*$/, "", lastAncName_c); # remove class name - sub(/^.*,/, "", lastAncName_c); # remove pre last ancestor name - noInterfaceIndices_c = lowerize(lastAncName_c) "Id"; - } else { - noInterfaceIndices_c = 0; - } - clsName_jni_c = "Wrapp" _fullClsName; - - if (printIntAndStb_c) { - outFile_int_c = createJavaFileName(clsName_int_c); - outFile_abs_c = createJavaFileName(clsName_abs_c); - outFile_stb_c = createJavaFileName(clsName_stb_c); - } - outFile_jni_c = createJavaFileName(clsName_jni_c); - - if (printIntAndStb_c) { - printHeader(outFile_int_c, myPkgA, clsName_int_c, 1, 0, 0, 0, 0); - printHeader(outFile_abs_c, myPkgA, clsName_abs_c, 0, clsName_int_c, 0, 1, 0); - printHeader(outFile_stb_c, myPkgA, clsName_stb_c, 0, clsName_int_c, 0, 0, clsName_abs_c); - } - printHeader( outFile_jni_c, myPkgA, clsName_jni_c, 0, clsName_int_c, 1, 0, clsName_abs_c); - - # prepare additional indices names - addInds_size_c = split(cls_implId_indicesArgs[implId_c], addInds_c, ","); - for (ai=1; ai <= addInds_size_c; ai++) { - sub(/int /, "", addInds_c[ai]); - addInds_c[ai] = trim(addInds_c[ai]); - } - - myInnerClb = myClassVar ".getInnerCallback()"; - - - # print private vars - print("\t" "private " myWrapClass " " myWrapVar " = null;") >> outFile_jni_c; - # print additionalVars - for (ai=1; ai <= addInds_size_c; ai++) { - print("\t" "private int " addInds_c[ai] " = -1;") >> outFile_jni_c; - } - print("") >> outFile_jni_c; - - - # print constructor - ctorParams = myWrapClass " " myWrapVar; - addIndPars_c = ""; - for (ai=1; ai <= addInds_size_c; ai++) { - addIndPars_c = addIndPars_c ", int " addInds_c[ai]; - } - ctorParams = ctorParams addIndPars_c; - ctorParamsNoTypes = removeParamTypes(ctorParams); - sub(/^, /, "", addIndPars_c); - addIndParsNoTypes_c = removeParamTypes(addIndPars_c); - condAddIndPars_c = (addIndPars_c == "") ? "" : ", "; - print("\t" "public " clsName_jni_c "(" ctorParams ") {") >> outFile_jni_c; - print("") >> outFile_jni_c; - print("\t\t" "this." myWrapVar " = " myWrapVar ";") >> outFile_jni_c; - # init additionalVars - for (ai=1; ai <= addInds_size_c; ai++) { - addIndName = addInds_c[ai]; - print("\t\t" "this." addIndName " = " addIndName ";") >> outFile_jni_c; - } - print("\t" "}") >> outFile_jni_c; - print("") >> outFile_jni_c; - - - # print additional vars fetchers - for (ai=1; ai <= addInds_size_c; ai++) { - addIndName = addInds_c[ai]; - addIndNameCap = capitalize(addIndName); - - printIntAndStb_tmp_c = printIntAndStb_c; - _noOverride = 0; - if ((noInterfaceIndices_c != 0) && (addIndName == noInterfaceIndices_c)) { - printIntAndStb_tmp_c = 0; - _noOverride = 1; - } - - # Print the direct fetcher function, eg. "int getUnitId()" - _fRet = "int"; - _fName = "get" addIndNameCap; - _fParams = ""; - _fExceps = ""; - _fIsDeprecated = 0; - printTripleFunc(_fRet, _fName, _fParams, _fExceps, outFile_int_c, outFile_stb_c, outFile_jni_c, printIntAndStb_tmp_c, _noOverride, _fIsDeprecated); - print("\t\t" "return " addIndName ";") >> outFile_jni_c; - print("\t" "}") >> outFile_jni_c; - print("") >> outFile_jni_c; - - # Print the OO entity fetcher function if applicable, eg. "Unit getUnit()" - addIndNameCapOO = addIndNameCap; - _hadId = sub(/Id$/, "", addIndNameCapOO); - if (_hadId && (addIndNameCapOO in cls_name_id) && (addIndNameCapOO != clsName_int_c)) { - _refObj = addIndNameCapOO; # example: Unit - _implId = implId_m "," _refObj; - if (_implId in cls_implId_fullClsName) { - _fullClsName = cls_implId_fullClsName[_implId]; - } else if (cls_name_implIds[_refObj ",*"] == 1) { - _fullClsName = cls_name_implIds[_refObj ",0"]; - _fullClsName = cls_implId_fullClsName[_fullClsName]; - } else { - print("ERROR: failed finding the full class name for: " _refObj); - exit(1); - } - - _wrappGetInst_params = myWrapVar; - for (aij=1; aij <= ai; aij++) { - _wrappGetInst_params = _wrappGetInst_params ", " addInds_c[aij]; - } - - _fRet = addIndNameCapOO; - _fName = "get" addIndNameCapOO; - _fParams = ""; - _fExceps = ""; - _fIsDeprecated = 0; - printTripleFunc(_fRet, _fName, _fParams, _fExceps, outFile_int_c, outFile_stb_c, outFile_jni_c, printIntAndStb_tmp_c, _noOverride, _fIsDeprecated); - print("\t\t" "return Wrapp" _fullClsName ".getInstance(" _wrappGetInst_params ");") >> outFile_jni_c; - print("\t" "}") >> outFile_jni_c; - print("") >> outFile_jni_c; - } - } - - # print static instance fetcher method - { - clsIsBuffered_c = isBufferedClass(clsName_c); - _isAvailableMethod = ""; - for (_memId in clsAvailInd_memberId_cls) { - if (clsAvailInd_memberId_cls[_memId] == clsName_c) { - _isAvailableMethod = _memId; - gsub(/,/, "_", _isAvailableMethod); - } - } - - if (clsIsBuffered_c) { - print("\t" "private static java.util.Map _buffer_instances = new java.util.HashMap();") >> outFile_jni_c; - print("") >> outFile_jni_c; - } - print("\t" "public static " clsName_c " getInstance(" ctorParams ") {") >> outFile_jni_c; - print("") >> outFile_jni_c; - lastParamName = ctorParamsNoTypes; - sub(/^.*,[ \t]*/, "", lastParamName); - if (match(lastParamName, /^[^ \t]+Id$/)) { - # id's < 0 are invalid, return null - print("\t\t" "if (" lastParamName " < 0) {") >> outFile_jni_c; - print("\t\t\t" "return null;") >> outFile_jni_c; - print("\t\t" "}") >> outFile_jni_c; - print("") >> outFile_jni_c; - } - print("\t\t" clsName_c " _ret = null;") >> outFile_jni_c; - if (_isAvailableMethod == "") { - print("\t\t" "_ret = new " clsName_jni_c "(" ctorParamsNoTypes ");") >> outFile_jni_c; - } else { - print("\t\t" "boolean isAvailable = " myWrapVar "." _isAvailableMethod "(" addIndParsNoTypes_c ");") >> outFile_jni_c; - print("\t\t" "if (isAvailable) {") >> outFile_jni_c; - print("\t\t\t" "_ret = new " clsName_jni_c "(" ctorParamsNoTypes ");") >> outFile_jni_c; - print("\t\t" "}") >> outFile_jni_c; - } - if (clsIsBuffered_c) { - if (_isAvailableMethod == "") { - print("\t\t" "{") >> outFile_jni_c; - } else { - print("\t\t" "if (_ret != null) {") >> outFile_jni_c; - } - print("\t\t\t" "Integer indexHash = _ret.hashCode();") >> outFile_jni_c; - print("\t\t\t" "if (_buffer_instances.containsKey(indexHash)) {") >> outFile_jni_c; - print("\t\t\t\t" "_ret = _buffer_instances.get(indexHash);") >> outFile_jni_c; - print("\t\t\t" "} else {") >> outFile_jni_c; - print("\t\t\t\t" "_buffer_instances.put(indexHash, _ret);") >> outFile_jni_c; - print("\t\t\t" "}") >> outFile_jni_c; - print("\t\t" "}") >> outFile_jni_c; - } - print("\t\t" "return _ret;") >> outFile_jni_c; - print("\t" "}") >> outFile_jni_c; - print("") >> outFile_jni_c; - } - - - if (printIntAndStb_c) { - # print compareTo(other) method - { - print("\t" "@Override") >> outFile_abs_c; - print("\t" "public int compareTo(" clsName_c " other) {") >> outFile_abs_c; - print("\t\t" "final int BEFORE = -1;") >> outFile_abs_c; - print("\t\t" "final int EQUAL = 0;") >> outFile_abs_c; - print("\t\t" "final int AFTER = 1;") >> outFile_abs_c; - print("") >> outFile_abs_c; - print("\t\t" "if (this == other) return EQUAL;") >> outFile_abs_c; - print("") >> outFile_abs_c; - - if (isClbRootCls) { - print("\t\t" "if (this.skirmishAIId < other.skirmishAIId) return BEFORE;") >> outFile_abs_c; - print("\t\t" "if (this.skirmishAIId > other.skirmishAIId) return AFTER;") >> outFile_abs_c; - print("\t\t" "return EQUAL;") >> outFile_abs_c; - } else { - for (ai=1; ai <= addInds_size_c; ai++) { - addIndName = addInds_c[ai]; - if ((noInterfaceIndices_c == 0) || (addIndName != noInterfaceIndices_c)) { - print("\t\t" "if (this.get" capitalize(addIndName) "() < other.get" capitalize(addIndName) "()) return BEFORE;") >> outFile_abs_c; - print("\t\t" "if (this.get" capitalize(addIndName) "() > other.get" capitalize(addIndName) "()) return AFTER;") >> outFile_abs_c; - } - } - print("\t\t" "return 0;") >> outFile_abs_c; - } - print("\t" "}") >> outFile_abs_c; - print("") >> outFile_abs_c; - } - - - # print equals(other) method - if (!isClbRootCls) { - print("\t" "@Override") >> outFile_abs_c; - print("\t" "public boolean equals(Object otherObject) {") >> outFile_abs_c; - print("") >> outFile_abs_c; - print("\t\t" "if (this == otherObject) return true;") >> outFile_abs_c; - print("\t\t" "if (!(otherObject instanceof " clsName_c ")) return false;") >> outFile_abs_c; - print("\t\t" clsName_c " other = (" clsName_c ") otherObject;") >> outFile_abs_c; - print("") >> outFile_abs_c; - - #if (isClbRootCls) { - # print("\t\t" "if (this.skirmishAIId != other.skirmishAIId) return false;") >> outFile_abs_c; - # print("\t\t" "return true;") >> outFile_abs_c; - #} - #else - { - for (ai=1; ai <= addInds_size_c; ai++) { - addIndName = addInds_c[ai]; - if ((noInterfaceIndices_c == 0) || (addIndName != noInterfaceIndices_c)) { - print("\t\t" "if (this.get" capitalize(addIndName) "() != other.get" capitalize(addIndName) "()) return false;") >> outFile_abs_c; - } - } - print("\t\t" "return true;") >> outFile_abs_c; - } - print("\t" "}") >> outFile_abs_c; - print("") >> outFile_abs_c; - } - - - # print hashCode() method - if (!isClbRootCls) { - print("\t" "@Override") >> outFile_abs_c; - print("\t" "public int hashCode() {") >> outFile_abs_c; - print("") >> outFile_abs_c; - - if (isClbRootCls) { - print("\t\t" "int _res = 0;") >> outFile_abs_c; - print("") >> outFile_abs_c; - print("\t\t" "_res += this.skirmishAIId * 10E8;") >> outFile_abs_c; - } else { - print("\t\t" "int _res = 23;") >> outFile_abs_c; - print("") >> outFile_abs_c; - # NOTE: This could go wrong if we have more then 7 additional indices - # see 10E" (7-ai) below - # the conversion to int is nessesarry, - # as otherwise it would be a double, - # which would be higher then max int, - # and most hashes would end up being max int, - # when converted from double to int - for (ai=1; ai <= addInds_size_c; ai++) { - addIndName = addInds_c[ai]; - if ((noInterfaceIndices_c == 0) || (addIndName != noInterfaceIndices_c)) { - print("\t\t" "_res += this.get" capitalize(addIndName) "() * (int) (10E" (7-ai) ");") >> outFile_abs_c; - } - } - } - print("") >> outFile_abs_c; - print("\t\t" "return _res;") >> outFile_abs_c; - print("\t" "}") >> outFile_abs_c; - print("") >> outFile_abs_c; - } - - - # print toString() method - { - print("\t" "@Override") >> outFile_abs_c; - print("\t" "public String toString() {") >> outFile_abs_c; - print("") >> outFile_abs_c; - print("\t\t" "String _res = this.getClass().toString();") >> outFile_abs_c; - print("") >> outFile_abs_c; - - #if (isClbRootCls) { # NO FOLD - # print("\t\t" "_res = _res + \"(skirmishAIId=\" + this.skirmishAIId + \", \";") >> outFile_abs_c; - #} else { # NO FOLD - # print("\t\t" "_res = _res + \"(clbHash=\" + this." myWrapVar ".hashCode() + \", \";") >> outFile_abs_c; - # print("\t\t" "_res = _res + \"skirmishAIId=\" + this." myWrapVar ".SkirmishAI_getSkirmishAIId() + \", \";") >> outFile_abs_c; - for (ai=1; ai <= addInds_size_c; ai++) { - addIndName = addInds_c[ai]; - if ((noInterfaceIndices_c == 0) || (addIndName != noInterfaceIndices_c)) { - print("\t\t" "_res = _res + \"" addIndName "=\" + this.get" capitalize(addIndName) "() + \", \";") >> outFile_abs_c; - } - } - #} # NO FOLD - print("\t\t" "_res = _res + \")\";") >> outFile_abs_c; - print("") >> outFile_abs_c; - print("\t\t" "return _res;") >> outFile_abs_c; - print("\t" "}") >> outFile_abs_c; - print("") >> outFile_abs_c; - } - } - - # make these available in called functions - implId_c_ = implId_c; - clsName_c_ = clsName_c; - printIntAndStb_c_ = printIntAndStb_c; - - # print member functions - members_size = cls_name_members[clsName_int_c ",*"]; - for (m=0; m < members_size; m++) { - memName_c = cls_name_members[clsName_int_c "," m]; - fullName_c = implId_c "," memName_c; - gsub(/,/, "_", fullName_c); - if (doWrappMember(fullName_c)) { - printMember(fullName_c, memName_c, addInds_size_c); - } else { - print("JavaOO-AIWrapper: NOTE: intentionally not wrapped: " fullName_c); - } - } - - - # finnish up - if (printIntAndStb_c) { - print("}") >> outFile_int_c; - print("") >> outFile_int_c; - close(outFile_int_c); - - print("}") >> outFile_abs_c; - print("") >> outFile_abs_c; - close(outFile_abs_c); - - print("}") >> outFile_stb_c; - print("") >> outFile_stb_c; - close(outFile_stb_c); - } - print("}") >> outFile_jni_c; - print("") >> outFile_jni_c; - close(outFile_jni_c); -} - - -function isRetParamName(paramName_rp) { - return (match(paramName_rp, /_out(_|$)/) || match(paramName_rp, /(^|_)?ret_/)); -} - - -function printMember(fullName_m, memName_m, additionalIndices_m) { - - # use some vars from the printClass function (which called us) - implId_m = implId_c_; - clsName_m = clsName_c_; - printIntAndStb_m = printIntAndStb_c_; - implCls_m = implCls_c; - clsName_int_m = clsName_int_c; - clsName_stb_m = clsName_stb_c; - clsName_jni_m = clsName_jni_c; - outFile_int_m = outFile_int_c; - outFile_stb_m = outFile_stb_c; - outFile_jni_m = outFile_jni_c; - addInds_size_m = addInds_size_c; - for (ai=1; ai <= addInds_size_m; ai++) { - addInds_m[ai] = addInds_c[ai]; - } - - indent_m = "\t"; - memId_m = clsName_m "," memName_m; - retType = cls_memberId_retType[memId_m]; # this may be changed - retType_int = retType; # this is a const var - params = cls_memberId_params[memId_m]; - isFetcher = cls_memberId_isFetcher[memId_m]; - metaComment = cls_memberId_metaComment[memId_m]; - memName = fullName_m; - sub(/^.*_/, "", memName); - functionName_m = fullName_m; - sub(/^[^_]+_/, "", functionName_m); - - if (memId_m in clsAvailInd_memberId_cls) { - return; - } - - isVoid_int_m = (retType_int == "void"); - - retVar_int_m = "_ret_int"; # this is a const var - retVar_out_m = retVar_int_m; # this may be changed - declaredVarsCode = ""; - conversionCode_pre = ""; - conversionCode_post = ""; - thrownExceptions = ""; - ommitMainCall = 0; - - if (!isVoid_int_m) { - declaredVarsCode = "\t\t" retType_int " " retVar_int_m ";" "\n" declaredVarsCode; - } - - - # Rewrite meta comment - if (match(metaComment, /FETCHER:MULTI:IDs:/)) { - # convert this: FETCHER:MULTI:IDs:Group:groupIds - # to this: ARRAY:groupIds->Group - _mc_pre = metaComment; - sub(/FETCHER:MULTI:IDs:.*$/, "", _mc_pre); - _mc_fet = metaComment; - sub(/^.*FETCHER:MULTI:IDs:/, "", _mc_fet); - sub(/[ \t].*$/, "", _mc_fet); - _mc_post = metaComment; - sub(/^.*FETCHER:MULTI:IDs:[^ \t]*/, "", _mc_post); - - _refObj = _mc_fet; - sub(/:.*$/, "", _refObj); - _refPaNa = _mc_fet; - sub(/^.*:/, "", _refPaNa); - - _mc_newArr = "ARRAY:" _refPaNa "->" _refObj; - metaComment = _mc_pre _mc_newArr _mc_post; - } - if (match(metaComment, /FETCHER:MULTI:NUM:/)) { - # convert this: FETCHER:MULTI:NUM:Resource - # to this: ARRAY:RETURN_SIZE->Resource - sub(/FETCHER:MULTI:NUM:/, "ARRAY:RETURN_SIZE->", metaComment); - } - if (match(metaComment, /REF:MULTI:/)) { - # convert this: REF:MULTI:unitDefIds->UnitDef - # to this: ARRAY:unitDefIds->UnitDef - sub(/REF:MULTI:/, "ARRAY:", metaComment); - } - - # remove additional indices from the outer params - for (ai=1; ai <= addInds_size_m; ai++) { - _removed = sub(/[^,]+(, )?/, "", params); - if (!_removed && !part_isStatic(memName_m, metaComment)) { - addIndName = addInds_m[ai]; - print("ERROR: failed removing additional indices " addIndName " from method " memName_m " in class " clsName_int_m); - exit(1); - } - } - - innerParams = removeParamTypes(params); - - # add additional indices fetcher calls to inner params - addInnerParams = ""; - addInds_real_size_m = addInds_size_m; - if (part_isStatic(memName_m, metaComment)) { - addInds_real_size_m--; - } - for (ai=1; ai <= addInds_real_size_m; ai++) { - addIndName = addInds_m[ai]; - _condComma = ""; - if (addInnerParams != "") { - _condComma = ", "; - } - addInnerParams = addInnerParams _condComma "this.get" capitalize(addIndName) "()"; - } - _condComma = ""; - if ((addInnerParams != "") && (innerParams != "")) { - _condComma = ", "; - } - innerParams = addInnerParams _condComma innerParams; - - - # convert param types - paramNames_size = split(innerParams, paramNames, ", "); - for (prm = 1; prm <= paramNames_size; prm++) { - paNa = paramNames[prm]; - if (!isRetParamName(paNa)) { - if (match(paNa, /_posF3/)) { - # convert float[3] to AIFloat3 - paNaNew = paNa; - sub(/_posF3/, "", paNaNew); - sub("float\\[\\] " paNa, "AIFloat3 " paNaNew, params); - conversionCode_pre = conversionCode_pre "\t\t" "float[] " paNa " = " paNaNew ".toFloatArray();" "\n"; - } else if (match(paNa, /_colorS3/)) { - # convert short[3] to java.awt.Color - paNaNew = paNa; - sub(/_colorS3/, "", paNaNew); - sub("short\\[\\] " paNa, "java.awt.Color " paNaNew, params); - conversionCode_pre = conversionCode_pre "\t\t" "short[] " paNa " = Util.toShort3Array(" paNaNew ");" "\n"; - } - } - } - - # convert an error return int value to an Exception - # "error-return:0=OK" - if (part_isErrorReturn(metaComment) && retType == "int") { - errorRetValueOk_m = part_getErrorReturnValueOk(metaComment); - - conversionCode_post = conversionCode_post "\t\t" "if (" retVar_out_m " != " errorRetValueOk_m ") {" "\n"; - conversionCode_post = conversionCode_post "\t\t\t" "throw new CallbackAIException(\"" memName_m "\", " retVar_out_m ");" "\n"; - conversionCode_post = conversionCode_post "\t\t" "}" "\n"; - thrownExceptions = thrownExceptions ", CallbackAIException" thrownExceptions; - - retType = "void"; - } - - # convert out params to return values - paramTypeNames_size = split(params, paramTypeNames, ", "); - hasRetParam = 0; - for (prm = 1; prm <= paramTypeNames_size; prm++) { - paNa = extractParamName(paramTypeNames[prm]); - if (isRetParamName(paNa)) { - if (retType == "void") { - paTy = extractParamType(paramTypeNames[prm]); - hasRetParam = 1; - if (match(paNa, /_posF3/)) { - # convert float[3] to AIFloat3 - retParamType = "AIFloat3"; - retVar_out_m = "_ret"; - conversionCode_pre = conversionCode_pre "\t\t" "float[] " paNa " = new float[3];" "\n"; - conversionCode_post = conversionCode_post "\t\t" retVar_out_m " = new AIFloat3(" paNa "[0], " paNa "[1]," paNa "[2]);" "\n"; - declaredVarsCode = "\t\t" retParamType " " retVar_out_m ";" "\n" declaredVarsCode; - sub("(, )?float\\[\\] " paNa, "", params); - retType = retParamType; - } else if (match(paNa, /_colorS3/)) { - retParamType = "java.awt.Color"; - retVar_out_m = "_ret"; - conversionCode_pre = conversionCode_pre "\t\t" "short[] " paNa " = new short[3];" "\n"; - conversionCode_post = conversionCode_post "\t\t" retVar_out_m " = Util.toColor(" paNa ");" "\n"; - declaredVarsCode = "\t\t" retParamType " " retVar_out_m ";" "\n" declaredVarsCode; - sub("(, )?short\\[\\] " paNa, "", params); - retType = retParamType; - } else if (match(paTy, /StringBuffer/)) { - retParamType = "String"; - retVar_out_m = "_ret"; - conversionCode_pre = conversionCode_pre "\t\t" "StringBuffer " paNa " = new StringBuffer();" "\n"; - conversionCode_post = conversionCode_post "\t\t" retParamType " " retVar_out_m " = " paNa ".toString();" "\n"; - sub("(, )?StringBuffer " paNa, "", params); - retType = retParamType; - } else { - print("FAILED converting return param: " paramTypeNames[prm] " / " fullName_m); - exit(1); - } - } else { - print("FAILED converting return param: return type should be \"void\", but is \"" retType "\""); - exit(1); - } - } - } - - # REF: - refObjs_size_m = split(metaComment, refObjs_m, "REF:"); - for (ro=2; ro <= refObjs_size_m; ro++) { - _ref = refObjs_m[ro]; - sub(/[ \t].*$/, "", _ref); # remove parts after this REF part - _isMulti = match(_ref, /MULTI:/); - _isReturn = match(_ref, /RETURN->/); - - if (!_isMulti && !_isReturn) { - # convert single param reference - _refRel = _ref; - sub(/^.*:/, "", _refRel); - _paNa = _refRel; # example: resourceId - sub(/->.*$/, "", _paNa); - _refObj = _refRel; # example: Resource - sub(/^.*->/, "", _refObj); - _paNaNew = _paNa; - if (!sub(/Id$/, "", _paNaNew)) { - _paNaNew = "oo_" _paNaNew; - } - - if (_refObj == "Team" || _refObj == "FigureGroup" || _refObj == "Path") { - print("note: ignoring meta comment: REF:" _ref); - } else { - _paNa_found = sub("int " _paNa, _refObj " " _paNaNew, params); - # it may not be found if it is an output parameter - if (_paNa_found) { - conversionCode_pre = conversionCode_pre "\t\t" "int " _paNa " = " _paNaNew ".get" _refObj "Id();" "\n"; - } - } - } else if (!_isMulti && _isReturn) { - _refObj = _ref; # example: Resource - sub(/^.*->/, "", _refObj); - - if (_refObj == "Team" || _refObj == "FigureGroup" || _refObj == "Path") { - print("note: ignoring meta comment: REF:" _ref); - continue; - } - - _implId = implId_m "," _refObj; - if (_implId in cls_implId_fullClsName) { - _fullClsName = cls_implId_fullClsName[_implId]; - } else if (cls_name_implIds[_refObj ",*"] == 1) { - _fullClsName = cls_name_implIds[_refObj ",0"]; - _fullClsName = cls_implId_fullClsName[_fullClsName]; - } else { - print("ERROR: failed finding the full class name for: " _refObj); - exit(1); - } - - _retVar_out_new = retVar_out_m "_out"; - _wrappGetInst_params = myWrapVar; - _hasRetInd = 0; - _inPa_size = split(innerParams, _, ","); - if (retType != "void" && (_inPa_size == addInds_size_m || _inPa_size == 0)) { - _hasRetInd = 1; - } - for (ai=1; ai <= (addInds_size_m-_hasRetInd); ai++) { - # Very hacky! too unmotivated for proper fix, sorry. - # proper fix would involve getting the parent of the wrapped - # class and using its additional indices - if ((functionName_m != "UnitDef_WeaponMount_getWeaponDef") && (functionName_m != "Unit_Weapon_getDef")) { - _wrappGetInst_params = _wrappGetInst_params ", " addInds_m[ai]; - } - } - if (retType != "void") { - _wrappGetInst_params = _wrappGetInst_params ", " retVar_out_m; - } else { - ommitMainCall = 1; - } - conversionCode_post = conversionCode_post "\t\t" _retVar_out_new " = Wrapp" _fullClsName ".getInstance(" _wrappGetInst_params ");" "\n"; - declaredVarsCode = "\t\t" _refObj " " _retVar_out_new ";" "\n" declaredVarsCode; - retVar_out_m = _retVar_out_new; - retType = _refObj; - } else { - print("WARNING: unsupported: REF:" _ref); - } - } - - - isMap = part_isMap(fullName_m, metaComment); - if (isMap) { - - _isFetching = 1; - _isRetSize = 0; - _isObj = 0; - _mapVar_size = "_size"; - _mapVar_keys = "keys"; - _mapVar_values = "values"; - _mapType_key = "String"; - _mapType_value = "String"; - _mapType_oo_key = "String"; - _mapType_oo_value = "String"; - _mapVar_oo = "_map"; - _mapType_int = "java.util.Map<" _mapType_oo_key ", " _mapType_oo_value ">"; - _mapType_impl = "java.util.HashMap<" _mapType_oo_key ", " _mapType_oo_value ">"; - - sub("(, )?" _mapType_key "\\[\\] " _mapVar_keys, "", params); - sub("(, )?" _mapType_value "\\[\\] " _mapVar_values, "", params); - sub(/, $/ , "", params); - - declaredVarsCode = "\t\t" "int " _mapVar_size ";" "\n" declaredVarsCode; - if (_isFetching) { - declaredVarsCode = "\t\t" _mapType_int " " _mapVar_oo ";" "\n" declaredVarsCode; - } - if (!_isRetSize) { - declaredVarsCode = "\t\t" _mapType_key "[] " _mapVar_keys ";" "\n" declaredVarsCode; - declaredVarsCode = "\t\t" _mapType_value "[] " _mapVar_values ";" "\n" declaredVarsCode; - if (_isFetching) { - conversionCode_pre = conversionCode_pre "\t\t" _mapVar_keys " = null;" "\n"; - conversionCode_pre = conversionCode_pre "\t\t" _mapVar_values " = null;" "\n"; - conversionCode_pre = conversionCode_pre "\t\t" _mapVar_size " = " myWrapVar "." functionName_m "(" innerParams ");" "\n"; - } else { - #conversionCode_pre = conversionCode_pre "\t\t" _arraySizeVar " = " _arrayListVar ".size();" "\n"; - #conversionCode_pre = conversionCode_pre "\t\t" "int _size = " _arraySizeVar ";" "\n"; - } - } - - if (_isRetSize) { - #conversionCode_post = conversionCode_post "\t\t" _arraySizeVar " = " retVar_out_m ";" "\n"; - #_arraySizeMaxPaNa = _arraySizeVar; - } else { - conversionCode_pre = conversionCode_pre "\t\t" _mapVar_keys " = new " _mapType_key "[" _mapVar_size "];" "\n"; - conversionCode_pre = conversionCode_pre "\t\t" _mapVar_values " = new " _mapType_value "[" _mapVar_size "];" "\n"; - } - - if (_isFetching) { - # convert to a HashMap - conversionCode_post = conversionCode_post "\t\t" _mapVar_oo " = new " _mapType_impl "();" "\n"; - conversionCode_post = conversionCode_post "\t\t" "for (int i=0; i < " _mapVar_size "; i++) {" "\n"; -# if (_isObj) { -# if (_isRetSize) { - conversionCode_post = conversionCode_post "\t\t\t" _mapVar_oo ".put(" _mapVar_keys "[i], " _mapVar_values "[i]);" "\n"; -# } else { - #conversionCode_post = conversionCode_post "\t\t\t" _mapVar_oo ".put(" myPkgA ".Wrapp" _refObj ".getInstance(" myWrapVar _addWrappVars ", " _arrayPaNa "[i]));" "\n"; -# } -# } else if (_isNative) { - #conversionCode_post = conversionCode_post "\t\t\t" _arrayListVar ".add(" _arrayPaNa "[i]);" "\n"; -# } - conversionCode_post = conversionCode_post "\t\t" "}" "\n"; - - retParamType = _mapType_int; - retVar_out_m = _mapVar_oo; - retType = retParamType; - } else { - # convert from a HashMap - } - } - - - isArray = part_isArray(fullName_m, metaComment); - if (isArray) { - _refObj = ""; - _arrayPaNa = metaComment; - _addWrappVars = ""; - sub(/^.*ARRAY:/, "", _arrayPaNa); - sub(/[ \t].*$/, "", _arrayPaNa); - if (match(_arrayPaNa, /->/)) { - _refObj = _arrayPaNa; - sub(/->.*$/, "", _arrayPaNa); - sub(/^.*->/, "", _refObj); - _refObjInt = _refObj; - - if (match(_refObj, /-/)) { - sub(/-.*$/, "", _refObj); - sub(/^.*-/, "", _refObjInt); - } - _implId = implId_m "," _refObj; - if (_implId in cls_implId_fullClsName) { - _fullClsName = cls_implId_fullClsName[_implId]; - } else if ((myRootClass "," _refObj) in cls_implId_fullClsName) { - _implId = myRootClass "," _refObj; - _fullClsName = cls_implId_fullClsName[_implId]; - } else { - print("ERROR: failed to find the full class name for " _refObj " in " fullName_m); - exit(1); - } - _refObj = _fullClsName; - _addWrappVars = cls_implId_indicesArgs[_implId]; - sub(/(,)?[^,]*$/, "", _addWrappVars); # remove last index - _addWrappVars = trim(removeParamTypes(_addWrappVars)); - if (_addWrappVars != "") { - _addWrappVars = ", " _addWrappVars; - } - } - - _isF3 = match(_arrayPaNa, /_AposF3/); - _isObj = (_refObj != ""); - _isNative = (!_refObj && !_isObjc); - - _isRetSize = 0; - if (_isObj) { - _isRetSize = (_arrayPaNa == "RETURN_SIZE"); - } - - _arrayType = params; - sub("\\[\\][ \t]" _arrayPaNa ".*$", "", _arrayType); - sub("^.*[ \t]", "", _arrayType); - _arraySizeMaxPaNa = _arrayPaNa "_sizeMax"; - _arraySizeVar = _arrayPaNa "_size"; - _arraySizeRaw = _arrayPaNa "_raw_size"; - - _arrayListVar = _arrayPaNa "_list"; - if (_isF3) { - _arrListGenType = "AIFloat3"; - } else if (_isObj) { - _arrListGenType = _refObjInt; - } else if (_isNative) { - _arrListGenType = convertJavaBuiltinTypeToClass(_arrayType); - } - _arrListType = "java.util.List<" _arrListGenType ">"; - _arrListImplType = "java.util.ArrayList<" _arrListGenType ">"; - - _isFetching = sub("(, )?int " _arraySizeMaxPaNa, "", params); - if (_isRetSize) { - _isFetching = 1; - } else { - if (!_isFetching && !_isRetSize) { - _isNonFetcher = sub("(, )?int " _arraySizeVar, "", params); - if (!_isNonFetcher) { - print("ERROR: neither propper fetcher nor supplier ARRAY syntax in function: " fullName_m); - exit(1); - } - } - if (_isFetching) { - sub(_arrayType "\\[\\] " _arrayPaNa, "", params); - } else { - sub(_arrayType "\\[\\] " _arrayPaNa, _arrListType " " _arrayListVar, params); - } - sub(/^, /, "", params); - sub(/, $/, "", params); - } - - declaredVarsCode = "\t\t" "int " _arraySizeVar ";" "\n" declaredVarsCode; - if (_isFetching) { - declaredVarsCode = "\t\t" _arrListType " " _arrayListVar ";" "\n" declaredVarsCode; - } - if (!_isRetSize) { - declaredVarsCode = "\t\t" _arrayType "[] " _arrayPaNa ";" "\n" declaredVarsCode; - declaredVarsCode = "\t\t" "int " _arraySizeRaw ";" "\n" declaredVarsCode; - if (_isFetching) { - declaredVarsCode = "\t\t" "int " _arraySizeMaxPaNa ";" "\n" declaredVarsCode; - conversionCode_pre = conversionCode_pre "\t\t" _arraySizeMaxPaNa " = Integer.MAX_VALUE;" "\n"; - conversionCode_pre = conversionCode_pre "\t\t" _arrayPaNa " = null;" "\n"; - conversionCode_pre = conversionCode_pre "\t\t" _arraySizeVar " = " myWrapVar "." functionName_m "(" innerParams ");" "\n"; - conversionCode_pre = conversionCode_pre "\t\t" _arraySizeMaxPaNa " = " _arraySizeVar ";" "\n"; - conversionCode_pre = conversionCode_pre "\t\t" _arraySizeRaw " = " _arraySizeVar ";" "\n"; - if (_isF3) { - conversionCode_pre = conversionCode_pre "\t\t" "if (" _arraySizeVar " % 3 != 0) {" "\n"; - conversionCode_pre = conversionCode_pre "\t\t\t" "throw new RuntimeException(\"returned AIFloat3 array has incorrect size (\" + " _arraySizeVar "+ \"), should be a multiple of 3.\");" "\n"; - conversionCode_pre = conversionCode_pre "\t\t" "}" "\n"; - conversionCode_pre = conversionCode_pre "\t\t" _arraySizeVar " /= 3;" "\n"; - } - } else { - conversionCode_pre = conversionCode_pre "\t\t" _arraySizeVar " = " _arrayListVar ".size();" "\n"; - conversionCode_pre = conversionCode_pre "\t\t" "int _size = " _arraySizeVar ";" "\n"; - if (_isF3) { - conversionCode_pre = conversionCode_pre "\t\t" _arraySizeVar " *= 3;" "\n"; - } - conversionCode_pre = conversionCode_pre "\t\t" _arraySizeRaw " = " _arraySizeVar ";" "\n"; - } - } - - if (_isRetSize) { - conversionCode_post = conversionCode_post "\t\t" _arraySizeVar " = " retVar_out_m ";" "\n"; - _arraySizeMaxPaNa = _arraySizeVar; - } else { - conversionCode_pre = conversionCode_pre "\t\t" _arrayPaNa " = new " _arrayType "[" _arraySizeRaw "];" "\n"; - } - - if (_isFetching) { - # convert to an ArrayList - conversionCode_post = conversionCode_post "\t\t" _arrayListVar " = new " _arrListImplType "(" _arraySizeVar ");" "\n"; - conversionCode_post = conversionCode_post "\t\t" "for (int i=0; i < " _arraySizeMaxPaNa "; i++) {" "\n"; - if (_isF3) { - conversionCode_post = conversionCode_post "\t\t\t" _arrayListVar ".add(new AIFloat3(" _arrayPaNa "[i], " _arrayPaNa "[++i], " _arrayPaNa "[++i]));" "\n"; - } else if (_isObj) { - if (_isRetSize) { - conversionCode_post = conversionCode_post "\t\t\t" _arrayListVar ".add(" myPkgA ".Wrapp" _refObj ".getInstance(" myWrapVar _addWrappVars ", i));" "\n"; - } else { - conversionCode_post = conversionCode_post "\t\t\t" _arrayListVar ".add(" myPkgA ".Wrapp" _refObj ".getInstance(" myWrapVar _addWrappVars ", " _arrayPaNa "[i]));" "\n"; - } - } else if (_isNative) { - conversionCode_post = conversionCode_post "\t\t\t" _arrayListVar ".add(" _arrayPaNa "[i]);" "\n"; - } - conversionCode_post = conversionCode_post "\t\t" "}" "\n"; - - retParamType = _arrListType; - retVar_out_m = _arrayListVar; - retType = retParamType; - } else { - # convert from an ArrayList - conversionCode_pre = conversionCode_pre "\t\t" "for (int i=0; i < _size; i++) {" "\n"; - if (_isF3) { - conversionCode_pre = conversionCode_pre "\t\t\t" "int arrInd = i*3;" "\n"; - conversionCode_pre = conversionCode_pre "\t\t\t" "AIFloat3 aif3 = " _arrayListVar ".get(i);" "\n"; - conversionCode_pre = conversionCode_pre "\t\t\t" _arrayPaNa "[arrInd] = aif3.x;" "\n"; - conversionCode_pre = conversionCode_pre "\t\t\t" _arrayPaNa "[arrInd+1] = aif3.y;" "\n"; - conversionCode_pre = conversionCode_pre "\t\t\t" _arrayPaNa "[arrInd+2] = aif3.z;" "\n"; - } else if (_isObj) { - conversionCode_pre = conversionCode_pre "\t\t\t" _arrayPaNa "[i] = " _arrayListVar ".get(i).get" _refObj "Id();" "\n"; - } else if (_isNative) { - conversionCode_pre = conversionCode_pre "\t\t\t" _arrayPaNa "[i] = " _arrayListVar ".get(i);" "\n"; - } - conversionCode_pre = conversionCode_pre "\t\t" "}" "\n"; - } - } - - firstLineEnd = ";"; - mod_m = ""; - if (!isInterface_m) { - firstLineEnd = " {"; - mod_m = "public "; - } - - sub(/^, /, "", thrownExceptions); - - print("") >> outFile_jni_m; - - isBuffered_m = !isVoid_m && isBufferedFunc(fullName_m) && (params == ""); - if (!isInterface_m && isBuffered_m) { - print(indent_m retType " _buffer_" memName ";") >> outFile_jni_m; - print(indent_m "boolean _buffer_isInitialized_" memName " = false;") >> outFile_jni_m; - } - - # print method doc comment - fullName_doc_m = fullName_m; - sub(/^[^_]*_/, "", fullName_doc_m); # remove OOAICallback_ - if (printIntAndStb_m) { - printFunctionComment_Common(outFile_int_m, funcDocComment, fullName_doc_m, indent_m); - printFunctionComment_Common(outFile_stb_m, funcDocComment, fullName_doc_m, indent_m); - } - printFunctionComment_Common(outFile_jni_m, funcDocComment, fullName_doc_m, indent_m); - - _fNoOverride = 0; - commentText = getFunctionComment_Common(funcDocComment, fullName_doc_m); - _fIsDeprecated = match(commentText, /@deprecated/); - printTripleFunc(retType, memName, params, thrownExceptions, outFile_int_m, outFile_stb_m, outFile_jni_m, printIntAndStb_m, _fNoOverride, _fIsDeprecated); - - isVoid_m = (retType == "void"); - - if (!isInterface_m) { - condRet_int_m = isVoid_int_m ? "" : retVar_int_m " = "; - indent_m = indent_m "\t"; - - if (isBuffered_m) { - print(indent_m "if (!_buffer_isInitialized_" memName ") {") >> outFile_jni_m; - indent_m = indent_m "\t"; - } - if (declaredVarsCode != "") { - print(declaredVarsCode) >> outFile_jni_m; - } - if (conversionCode_pre != "") { - print(conversionCode_pre) >> outFile_jni_m; - } - if (!ommitMainCall) { - print(indent_m condRet_int_m myWrapVar "." functionName_m "(" innerParams ");") >> outFile_jni_m; - } - if (conversionCode_post != "") { - print(conversionCode_post) >> outFile_jni_m; - } - if (isBuffered_m) { - print(indent_m "_buffer_" memName " = " retVar_out_m ";") >> outFile_jni_m; - print(indent_m "_buffer_isInitialized_" memName " = true;") >> outFile_jni_m; - sub(/\t/, "", indent_m); - print(indent_m "}") >> outFile_jni_m; - print("") >> outFile_jni_m; - retVar_out_m = "_buffer_" memName; - } - if (!isVoid_m) { - print(indent_m "return " retVar_out_m ";") >> outFile_jni_m; - } - sub(/\t/, "", indent_m); - print(indent_m "}") >> outFile_jni_m; - } -} - - -function doWrappMember(fullName_dwm) { - - doWrapp_dwm = 1; - - return doWrapp_dwm; -} - -# Used by the common OO AWK script -function doWrappOO(funcFullName_dw, params_dw, metaComment_dw) { - - doWrapp_dw = 1; - - #doWrapp_dw = doWrapp_dw && !match(funcFullName_dw, /Lua_callRules/) && !match(funcFullName_dw, /Lua_callUI/); - - return doWrapp_dw; -} - -function wrappFunctionDef(funcDef, commentEolTot) { - - size_funcParts = split(funcDef, funcParts, "(\\()|(\\);)"); - # because the empty part after ");" would count as part as well - size_funcParts--; - - fullName = funcParts[1]; - fullName = trim(fullName); - sub(/.*[ \t]+/, "", fullName); - - retType = funcParts[1]; - sub(/[ \t]*public/, "", retType); - sub(fullName, "", retType); - retType = trim(retType); - - params = funcParts[2]; - - wrappFunctionPlusMeta(retType, fullName, params, commentEolTot); -} - -# This function has to return true (1) if a doc comment (eg: /** foo bar */) -# can be deleted. -# If there is no special condition you want to apply, -# it should always return true (1), -# cause there are additional mechanism to prevent accidental deleting. -# see: commonDoc.awk -function canDeleteDocumentation() { - return isMultiLineFunc != 1; -} - - -# grab callback functions info -# 2nd, 3rd, ... line of a function definition -{ - if (isMultiLineFunc) { # function is defined on one single line - funcIntermLine = $0; - # separate possible comment at end of line: // fu bar - commentEol = funcIntermLine; - if (sub(/.*\/\//, "", commentEol)) { - commentEolTot = commentEolTot commentEol; - } - sub(/[ \t]*\/\/.*$/, "", funcIntermLine); - funcIntermLine = trim(funcIntermLine); - funcSoFar = funcSoFar " " funcIntermLine; - if (match(funcSoFar, /;$/)) { - # function ends in this line - wrappFunctionDef(funcSoFar, commentEolTot); - isMultiLineFunc = 0; - } - } -} -# 1st line of a function definition -/\tpublic .*\);/ { - - funcStartLine = $0; - # separate possible comment at end of line: // foo bar - commentEolTot = ""; - commentEol = funcStartLine; - if (sub(/.*\/\//, "", commentEol)) { - commentEolTot = commentEolTot commentEol; - } - # remove possible comment at end of line: // foo bar - sub(/\/\/.*$/, "", funcStartLine); - funcStartLine = trim(funcStartLine); - if (match(funcStartLine, /;$/)) { - # function ends in this line - wrappFunctionDef(funcStartLine, commentEolTot); - } else { - funcSoFar = funcStartLine; - isMultiLineFunc = 1; - } -} - - - -END { - # finalize things - store_everything(); - printClasses(); -} diff --git a/AI/Wrappers/JavaOO/bin/wrappEvents.awk b/AI/Wrappers/JavaOO/bin/wrappEvents.awk deleted file mode 100755 index d044b135740..00000000000 --- a/AI/Wrappers/JavaOO/bin/wrappEvents.awk +++ /dev/null @@ -1,627 +0,0 @@ -#!/usr/bin/awk -f -# -# This awk script creates a java class in OO style to wrap the C style -# JNI based AI Events wrapper interface. -# In other words, the output of this file wraps: -# com/springrts/ai/AI.java -# which wraps: -# rts/ExternalAI/Interface/AISEvents.h -# -# This script uses functions from the following files: -# * common.awk -# * commonDoc.awk -# * commonOOCallback.awk -# Variables that can be set on the command-line (with -v): -# * GENERATED_SOURCE_DIR : the generated sources root dir -# * JAVA_GENERATED_SOURCE_DIR : the generated java sources root dir -# * INTERFACE_SOURCE_DIR : the Java AI Interfaces static source files root dir -# * INTERFACE_GENERATED_SOURCE_DIR : the Java AI Interfaces generated source files root dir -# - -BEGIN { - # initialize things - - # define the field splitter(-regex) - FS = "(\\()|(\\);)"; - IGNORECASE = 0; - - # Used by other scripts - JAVA_MODE = 1; - - # These vars can be assigned externally, see file header. - # Set the default values if they were not supplied on the command line. - if (!GENERATED_SOURCE_DIR) { - GENERATED_SOURCE_DIR = "../src-generated/main"; - } - if (!JAVA_GENERATED_SOURCE_DIR) { - JAVA_GENERATED_SOURCE_DIR = GENERATED_SOURCE_DIR "/java"; - } - if (!INTERFACE_SOURCE_DIR) { - INTERFACE_SOURCE_DIR = "../../../Interfaces/Java/src/main/java"; - } - if (!INTERFACE_GENERATED_SOURCE_DIR) { - INTERFACE_GENERATED_SOURCE_DIR = "../../../Interfaces/Java/src-generated/main/java"; - } - - javaSrcRoot = "../src/main/java"; - - myParentPkgA = "com.springrts.ai"; - myMainPkgA = myParentPkgA ".oo"; - myPkgClbA = myMainPkgA ".clb"; - myPkgEvtA = myMainPkgA ".evt"; - myMainPkgD = convertJavaNameFormAToD(myMainPkgA); - myPkgEvtD = convertJavaNameFormAToD(myPkgEvtA); - - aiFloat3Class = "AIFloat3"; - - myOOAIClass = "OOAI"; - myOOAIInterface = "I" myOOAIClass; - myOOAIAbstractClass = "AbstractOOAI"; - myOOEventAIClass = "OOEventAI"; - myOOEventAIInterface = "I" myOOEventAIClass; - myOOAIFile = JAVA_GENERATED_SOURCE_DIR "/" myMainPkgD "/" myOOAIClass ".java"; - myOOAIInterfaceFile = JAVA_GENERATED_SOURCE_DIR "/" myMainPkgD "/" myOOAIInterface ".java"; - myOOAIAbstractFile = JAVA_GENERATED_SOURCE_DIR "/" myMainPkgD "/" myOOAIAbstractClass ".java"; - myOOEventAIFile = JAVA_GENERATED_SOURCE_DIR "/" myMainPkgD "/" myOOEventAIClass ".java"; - myOOEventAIInterfaceFile = JAVA_GENERATED_SOURCE_DIR "/" myMainPkgD "/" myOOEventAIInterface ".java"; - - printOOAIHeader(myOOAIFile, myOOAIClass); - printOOAIHeader(myOOAIInterfaceFile, myOOAIInterface); - printOOAIHeader(myOOAIAbstractFile, myOOAIAbstractClass); - printOOEventAIHeader(myOOEventAIFile); - printOOEventAIHeader(myOOEventAIInterfaceFile); - - ind_evt = 0; -} - - - -function printOOAIHeader(outFile, clsName) { - - printCommentsHeader(outFile); - print("") >> outFile; - print("package " myMainPkgA ";") >> outFile; - print("") >> outFile; - print("") >> outFile; - if (clsName != myOOAIInterface) { - print("import " myParentPkgA ".AI;") >> outFile; - } - if (clsName == myOOAIClass) { - print("import " myParentPkgA ".AICallback;") >> outFile; - print("import " myMainPkgA ".clb.WrappOOAICallback;") >> outFile; - print("import " myMainPkgA ".clb.WrappUnit;") >> outFile; - print("import " myMainPkgA ".clb.WrappWeaponDef;") >> outFile; - } - print("import " myMainPkgA ".AIFloat3;") >> outFile; - print("import " myMainPkgA ".clb.OOAICallback;") >> outFile; - print("import " myMainPkgA ".clb.Unit;") >> outFile; - print("import " myMainPkgA ".clb.WeaponDef;") >> outFile; - print("") >> outFile; - print("/**") >> outFile; - print(" * TODO: Add description here") >> outFile; - print(" *") >> outFile; - print(" * @author hoijui") >> outFile; - print(" * @version GENERATED") >> outFile; - print(" */") >> outFile; - - _type = "abstract class"; - _extends = ""; - _implements = " implements " myOOAIInterface ", AI"; - if (clsName == myOOAIAbstractClass) { - _extends = " extends " myOOAIClass; - } - if (clsName == myOOAIInterface) { - _type = "interface"; - _implements = ""; - } - print("public " _type " " clsName _extends _implements " {") >> outFile; - print("") >> outFile; - - if (clsName == myOOAIClass) { - print("\t" "private AICallback clb = null;") >> outFile; - print("\t" "private OOAICallback clbOO = null;") >> outFile; - print("") >> outFile; - } -} -function printOOAIEnd(outFile) { - - print("}") >> outFile; - print("") >> outFile; -} - -function printOOEventAIHeader(outFile) { - - printCommentsHeader(outFile); - print("") >> outFile; - print("package " myMainPkgA ";") >> outFile; - print("") >> outFile; - print("") >> outFile; - print("import " myParentPkgA ".AI;") >> outFile; - print("import " myMainPkgA "." myOOAIClass ";") >> outFile; - print("import " myPkgEvtA ".*;") >> outFile; - print("import " myPkgClbA ".*;") >> outFile; - print("") >> outFile; - print("/**") >> outFile; - print(" * TODO: Add description here") >> outFile; - print(" *") >> outFile; - print(" * @author hoijui") >> outFile; - print(" * @version GENERATED") >> outFile; - print(" */") >> outFile; - if (outFile == myOOEventAIFile) { - print("public abstract class " myOOEventAIClass " extends " myOOAIClass " implements " myOOEventAIInterface ", AI {") >> outFile; - } else { - print("public interface " myOOEventAIInterface " {") >> outFile; - } - print("") >> outFile; - - print("\t" "/**") >> outFile; - print("\t" " * TODO: Add description here") >> outFile; - print("\t" " *") >> outFile; - print("\t" " * @param event the AI event to handle, sent by the engine") >> outFile; - print("\t" " * @throws AIException") >> outFile; - print("\t" " */") >> outFile; - _modifyer = ""; - if (outFile == myOOEventAIFile) { - _modifyer = " abstract"; - } - print("\t" "public" _modifyer " void handleEvent(AIEvent event) throws EventAIException;") >> outFile; - print("") >> outFile; -} -function printOOEventAIEnd(outFile) { - - print("}") >> outFile; - print("") >> outFile; -} - - -function convertJavaSimpleTypeToOO(paType_sto, paName_sto, - paTypeNew_sto, paNameNew_sto, convPre_sto, convPost_sto) { - - _change = 1; - - # uses this global vars: - # - paramTypeNew - # - paramNameNew - # - conversionCode_pre - # - conversionCode_post - - paramTypeNew = paType_sto; - paramNameNew = paName_sto; - - if (match(paName_sto, /_posF3/)) { - # convert float[3] to AIFloat3 - sub(/_posF3/, "", paramNameNew); - paramTypeNew = "AIFloat3"; - conversionCode_pre = conversionCode_pre "\t\t" paramTypeNew " " paramNameNew " = new " paramTypeNew "(" paName_sto ");" "\n"; - #conversionCode_pre = conversionCode_pre "\t\t" paType_sto " " paName_sto " = " paNameNew_sto ".toFloatArray();" "\n"; - } else if (match(paName_sto, /_colorS3/)) { - # convert short[3] to java.awt.Color - sub(/_colorS3/, "", paramNameNew); - paramTypeNew = "java.awt.Color"; - conversionCode_pre = conversionCode_pre "\t\t" paramTypeNew " " paramNameNew " = Util.toColor(" paName_sto ");" "\n"; - #conversionCode_pre = conversionCode_pre "\t\t" paType_sto " " paName_sto " = Util.toShort3Array(" paNameNew_sto ");" "\n"; - } else if ((paType_sto == "int") && match(paName_sto, /(unit|builder|attacker|enemy)(Id)?$/)) { - # convert int to Unit - sub(/Id$/, "", paramNameNew); - paramNameNew = "oo_" paramNameNew; - paramTypeNew = "Unit"; - conversionCode_pre = conversionCode_pre "\t\t" paramTypeNew " " paramNameNew " = Wrapp" paramTypeNew ".getInstance(this.clb, " paName_sto ");" "\n"; - } else if ((paType_sto == "int[]") && match(paName_sto, /unit(s|Ids)$/)) { - # convert int[] to List - sub(/(Id)?s$/, "s", paramNameNew); - paramNameNew = "oo_" paramNameNew; - paramTypeNew = "java.util.List"; - conversionCode_pre = conversionCode_pre "\t\t" paramTypeNew " " paramNameNew " = new java.util.ArrayList();" "\n"; - conversionCode_pre = conversionCode_pre "\t\t" "for (int u=0; u < " paName_sto ".length; u++) {" "\n"; - conversionCode_pre = conversionCode_pre "\t\t\t" paramNameNew ".add(WrappUnit.getInstance(this.clb, " paName_sto "[u]));" "\n"; - conversionCode_pre = conversionCode_pre "\t\t" "}" "\n"; - } else if ((paType_sto == "int") && match(paName_sto, /(weaponDef)(Id)?$/)) { - # convert int to WeaponDef - sub(/Id$/, "", paramNameNew); - paramNameNew = "oo_" paramNameNew; - paramTypeNew = "WeaponDef"; - conversionCode_pre = conversionCode_pre "\t\t" paramTypeNew " " paramNameNew " = Wrapp" paramTypeNew ".getInstance(this.clb, " paName_sto ");" "\n"; - } else if (paType_sto == "AICallback") { - # convert AICallback to OOAICallback - paramNameNew = "oo_" paramNameNew; - paramTypeNew = "OOAICallback"; - conversionCode_pre = conversionCode_pre "\t\t" "this.clb = " paName_sto ";" "\n"; - conversionCode_pre = conversionCode_pre "\t\t" "this.clbOO = Wrapp" paramTypeNew ".getInstance(" paName_sto ");" "\n"; - conversionCode_pre = conversionCode_pre "\t\t" paramTypeNew " " paramNameNew " = this.clbOO;" "\n"; - } else { - _change = 0; - } - - return _change; -} - - -function printEventsOO() { - - # agarra te los event interfaces - for (e=0; e < evts_size; e++) { - meta_es = evts_meta[e]; - - interfList_size_es = 0; - if (match(meta_es, /INTERFACES:/)) { - interfMeta_es = meta_es; - sub(/^.*INTERFACES:/, "", interfMeta_es); - sub(/[ \t].*$/, "", interfMeta_es); - interfList_size_es = split(interfMeta_es, interfList_es, /\),/); - - for (i=1; i <= interfList_size_es; i++) { - _intName = interfList_es[i]; - sub(/\(.*$/, "", _intName); - _intParams = interfList_es[i]; - sub(/^.*\(/, "", _intParams); - sub(/\)$/, "", _intParams); - - if (!(_intName in int_names)) { - int_names[_intName] = _intParams; - } - evts_intNames[e "," (i-1)] = _intName; - evts_intParams[e "," (i-1)] = _intParams; - } - } - evts_intNames[e ",*"] = interfList_size_es; - } - - # print the event classes - for (e=0; e < evts_size; e++) { - printEventOO(e); - } - - # print the event interfaces - for (intName in int_names) { - printOOEventInterface(intName); - } -} - -function printEventOO(ind_evt_em) { - - retType_em = evts_retType[ind_evt_em]; - name_em = evts_name[ind_evt_em]; - params_em = evts_params[ind_evt_em]; - meta_em = evts_meta[ind_evt_em]; - - paramsList_size_em = split(params_em, paramsList_em, ", "); - - conversionCode_pre = ""; - conversionCode_post = ""; - ooParams_em = ""; - - for (p=1; p <= paramsList_size_em; p++) { - _param = paramsList_em[p]; - _paramType = _param; - sub(/ [^ ]*$/, "", _paramType); - _paramName = _param; - sub(/^[^ ]* /, "", _paramName); - - paramTypeNew = ""; - paramNameNew = ""; - convertJavaSimpleTypeToOO(_paramType, _paramName); - - if (!match(_paramName, /_size$/)) { - ooParams_em = ooParams_em ", " paramTypeNew " " paramNameNew; - } - } - sub(/^, /, "", ooParams_em); - - _equalMethod = (ooParams_em == params_em); - _isVoid = (retType_em == "void"); - if (_isVoid) { - _condRet = ""; - } else { - _condRet = "_ret = "; - } - - print("") >> myOOAIFile; - - if (!_equalMethod) { - ooParamNames_em = removeParamTypes(ooParams_em); - print("\t" "@Override") >> myOOAIFile; - print("\t" "public final " retType_em " " name_em "(" params_em ") {") >> myOOAIFile; - print("") >> myOOAIFile; - - if (!_isVoid) { - print("\t\t" retType_em " _ret;") >> myOOAIFile; - print("") >> myOOAIFile; - } - if (conversionCode_pre != "") { - print(conversionCode_pre) >> myOOAIFile; - } - - print("\t\t" _condRet "this." name_em "(" ooParamNames_em ");") >> myOOAIFile; - - if (conversionCode_post != "") { - print(conversionCode_post) >> myOOAIFile; - } - if (!_isVoid) { - print("") >> myOOAIFile; - print("\t\t" "return _ret;") >> myOOAIFile; - } - - print("\t" "}") >> myOOAIFile; - } - - printFunctionComment_Common(myOOAIFile, evts_docComment, ind_evt_em, "\t"); - print("\t" "@Override") >> myOOAIFile; - if (retType_em == "int") { - print("\t" "public " retType_em " " name_em "(" ooParams_em ") { return 0; }") >> myOOAIFile; - } else { - print("Warning: No default return value given for event return-type " retType_em); - print("\t" "public abstract " retType_em " " name_em "(" ooParams_em ");") >> myOOAIFile; - } - - ooParamsCleaned_em = ooParams_em; - gsub(/ oo_/, " ", ooParamsCleaned_em); - - print("") >> myOOAIInterfaceFile; - printFunctionComment_Common(myOOAIInterfaceFile, evts_docComment, ind_evt_em, "\t"); - print("\t" "public " retType_em " " name_em "(" ooParamsCleaned_em ");") >> myOOAIInterfaceFile; - - print("") >> myOOAIAbstractFile; - printFunctionComment_Common(myOOAIAbstractFile, evts_docComment, ind_evt_em, "\t"); - print("\t" "@Override") >> myOOAIAbstractFile; - print("\t" "public " retType_em " " name_em "(" ooParamsCleaned_em ") {") >> myOOAIAbstractFile; - print("\t\t" "return 0; // signaling: OK") >> myOOAIAbstractFile; - print("\t" "}") >> myOOAIAbstractFile; - - printOOEventWrapper(retType_em, name_em, ooParamsCleaned_em, meta_em, ind_evt_em); -} - -function printOOEventWrapper(retType_ei, mthName_ei, ooParams_ei, meta_ei, ind_evt_ei) { - - outFile = myOOEventAIFile; - ooParamsNoTypes_ei = removeParamTypes(ooParams_ei); - evtName_ei = capitalize(mthName_ei) "AIEvent"; - - print("\t" "@Override") >> outFile; - print("\t" "public final " retType_ei " " mthName_ei "(" ooParams_ei ") {") >> outFile; - print("") >> outFile; - print("\t\t" "AIEvent evt = new " evtName_ei "(" ooParamsNoTypes_ei ");") >> outFile; - print("\t\t" "try {") >> outFile; - print("\t\t\t" "this.handleEvent(evt);") >> outFile; - print("\t\t\t" "return 0; // everything OK") >> outFile; - print("\t\t" "} catch (EventAIException ex) {") >> outFile; - print("\t\t\t" "return ex.getErrorNumber();") >> outFile; - print("\t\t" "}") >> outFile; - print("\t" "}") >> outFile; - print("") >> outFile; - - printOOEventClass(retType_ei, evtName_ei, ooParams_ei, meta_ei, ind_evt_ei); -} - -function printOOEventClass(retType_ec, evtName_ec, ooParams_ec, meta_ec, ind_evt_ec) { - - outFile = JAVA_GENERATED_SOURCE_DIR "/" myPkgEvtD "/" evtName_ec ".java"; - - ooParamsList_size_ec = split(ooParams_ec, ooParamsList_ec, ", "); - int_size_ec = evts_intNames[ind_evt_ec ",*"]; - - # list up interface parameters - # clear arrays - split("", intParams_toPrint_ec); - split("", myIntParams_intParams_ec); - split("", myIntParams_int_ec); - for (_ii=0; _ii < int_size_ec; _ii++) { - _name = evts_intNames[ind_evt_ec "," _ii]; - _myIntParams = evts_intParams[ind_evt_ec "," _ii]; - _intParams = int_names[_name]; - - _myIntParamsList_size = split(_myIntParams, _myIntParamsList, ","); - _intParamsList_size = split(_intParams, _intParamsList, ","); - - for (_p=1; _p <= _intParamsList_size; _p++) { - _myIntParam = _myIntParamsList[_p]; - _intParam = _intParamsList[_p]; - - intParams_toPrint_ec[_intParam] = 1; - myIntParams_int_ec[_myIntParam] = _name; - if (_myIntParam != _intParam) { - myIntParams_intParams_ec[_myIntParam] = _intParam; - } - } - } - - _addIntLst = ""; - for (i=0; i < int_size_ec; i++) { - _addIntLst = _addIntLst ", " evts_intNames[ind_evt_ec "," i] "AIEvent"; - } - - # print comments header - printCommentsHeader(outFile); - print("") >> outFile; - - print("package " myPkgEvtA ";") >> outFile; - print("") >> outFile; - print("") >> outFile; - - # print imports - print("import " myMainPkgA ".AIEvent;") >> outFile; - print("import " myMainPkgA ".AIFloat3;") >> outFile; - print("import " myPkgClbA ".*;") >> outFile; - print("") >> outFile; - - # print class header - printFunctionComment_Common(outFile, evts_docComment, ind_evt_ec, ""); - print("public class " evtName_ec " implements AIEvent" _addIntLst " {") >> outFile; - print("") >> outFile; - - # print member vars - for (_p=1; _p <= ooParamsList_size_ec; _p++) { - print("\t" "private " ooParamsList_ec[_p] ";") >> outFile; - } - print("") >> outFile; - - # print constructor - print("\t" "public " evtName_ec "(" ooParams_ec ") {") >> outFile; - print("") >> outFile; - for (_p=1; _p <= ooParamsList_size_ec; _p++) { - _name = extractParamName(ooParamsList_ec[_p]); - print("\t\t" "this." _name " = " _name ";") >> outFile; - } - print("\t" "}") >> outFile; - print("") >> outFile; - - # print member getters - for (_p=1; _p <= ooParamsList_size_ec; _p++) { - _type = extractParamType(ooParamsList_ec[_p]); - _name = extractParamName(ooParamsList_ec[_p]); - - # save the type for later writing of interfaces, - # in case it is a ninterface param - if (_name in intParams_toPrint_ec) { - _intName = _name; - if (_name in myIntParams_intParams_ec) { - _intName = myIntParams_intParams_ec[_name]; - } - _int = myIntParams_int_ec[_name]; - - int_name_param_type[_int "," _intName] = _type; - # hacky - int_name_param_type[_int "," _name] = _type; - } - - # print out @Override if this is an interface member - if (_name in intParams_toPrint_ec && intParams_toPrint_ec[_name] == 1) { - print("\t" "@Override") >> outFile; - intParams_toPrint_ec[_name] = 0; - } - - print("\t" "public " _type " get" capitalize(_name) "() {") >> outFile; - print("\t\t" "return this." _name ";") >> outFile; - print("\t" "}") >> outFile; - - # print out another getter if the interface param name is different - if (_name in myIntParams_intParams_ec) { - _intName = myIntParams_intParams_ec[_name]; - intParams_toPrint_ec[_intName] = 0; - - print("\t" "@Override") >> outFile; - print("\t" "public " _type " get" capitalize(_intName) "() {") >> outFile; - print("\t\t" "return this.get" capitalize(_name) "();") >> outFile; - print("\t" "}") >> outFile; - } - - print("") >> outFile; - } - - print("}") >> outFile; - - close(outFile); -} - -function printOOEventInterface(int_name_ei) { - - outFile = JAVA_GENERATED_SOURCE_DIR "/" myPkgEvtD "/" int_name_ei "AIEvent.java"; - - int_params_ei = int_names[int_name_ei]; - - int_paramsList_size_ei = split(int_params_ei, int_paramsList_ei, ","); - int_size_ei = evts_intNames[ind_evt_ec ",*"]; - - _addIntLst = ""; - for (i=0; i < int_size_ei; i++) { - _addIntLst = _addIntLst ", " evts_intNames[ind_evt_ec "," i] "AIEvent"; - } - - # print comments header - printCommentsHeader(outFile); - print("") >> outFile; - - print("package " myPkgEvtA ";") >> outFile; - print("") >> outFile; - print("") >> outFile; - - # print imports - print("import " myMainPkgA ".AIEvent;") >> outFile; - print("import " myMainPkgA ".AIFloat3;") >> outFile; - print("import " myPkgClbA ".*;") >> outFile; - print("") >> outFile; - - # print class header - print("public interface " int_name_ei "AIEvent extends AIEvent {") >> outFile; - print("") >> outFile; - - # print getters - for (_p=1; _p <= int_paramsList_size_ei; _p++) { - _name = int_paramsList_ei[_p]; - _type = int_name_param_type[int_name_ei "," _name]; - - print("\t" "public " _type " get" capitalize(_name) "();") >> outFile; - } - print("") >> outFile; - - print("}") >> outFile; -} - - - -# This function has to return true (1) if a doc comment (eg: /** foo bar */) -# can be deleted. -# If there is no special condition you want to apply, -# it should always return true (1), -# cause there are additional mechanism to prevent accidental deleting. -# see: commonDoc.awk -function canDeleteDocumentation() { - return 1; -} - -################################################################################ -### BEGIN: parsing and saving the event methods - -# beginning of struct S*Event -/^\tpublic .+\);/ { - - _head = $1; - _params = $2; - _tail = $3; - - _retType = _head; - sub(/^\tpublic /, "", _retType); # remove pre - sub(/ .*$/, "", _retType); # remove post - - _name = _head; - sub(/^\tpublic [^ ]+ /, "", _name); # remove pre - - _meta = $0; - _hasMeta = sub(/.*\);[ \t]*\/\/[ \t]*/, "", _meta); # remove pre - if (!_hasMeta) { - _meta = ""; - } - - evts_retType[ind_evt] = _retType; - evts_name[ind_evt] = _name; - evts_params[ind_evt] = _params; - evts_meta[ind_evt] = _meta; - storeDocLines(evts_docComment, ind_evt); -#print(_retType " " _name "(" _params ") // " _meta); - ind_evt++; -} - -### END: parsing and saving the event methods -################################################################################ - - - - -END { - # finalize things - - evts_size = ind_evt; - - printEventsOO(); - - printOOAIEnd(myOOAIFile); - printOOAIEnd(myOOAIInterfaceFile); - printOOAIEnd(myOOAIAbstractFile); - printOOEventAIEnd(myOOEventAIFile); - printOOEventAIEnd(myOOEventAIInterfaceFile); - - close(myOOAIFile); - close(myOOAIInterfaceFile); - close(myOOAIAbstractFile); - close(myOOEventAIFile); - close(myOOEventAIInterfaceFile); -} diff --git a/AI/Wrappers/JavaOO/jlib/vecmath-src.jar b/AI/Wrappers/JavaOO/jlib/vecmath-src.jar deleted file mode 100644 index 4f66594635f784da70817fee98ebc6bb52db0d5e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 183017 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%0DqRNwh+>h1nLb1k)uYG&jotYyuUgY-9d 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

~$9-_LL;s_CJzdidj86~|=WrK@9 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%0DS~Nj{|5_2uNK_joS(Ay8~=U}Kr*b_+n2ln($a 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 - 4.0.0 - - - - - - 0.1 - - - - com.springrts - common-spring - 1.0 - ../../../rts/build/maven/support/common/spring/pom.xml - - - com.springrts - ai-wrapper-javaoo - ${my.version} - - jar - - Java OO AI Wrapper - Java Object Oriented Artificial Intelligence interface wrapper for the Spring RTS engine - https://springrts.com/wiki/AIWrapper:JavaOO - 2008 - - - - - scm:git:git://github.com/spring/spring.git - scm:git:git@github.com:spring/spring.git - http://github.com/spring/spring/tree/master/AI/Wrappers/JavaOO/ - - - - - - - com.springrts - ai-interface-java - ${my.version} - - - - java3d - vecmath - 1.3.1 - - - - - diff --git a/AI/Wrappers/JavaOO/src/main/java/com/springrts/ai/oo/AIEvent.java b/AI/Wrappers/JavaOO/src/main/java/com/springrts/ai/oo/AIEvent.java deleted file mode 100644 index 796112ef8a1..00000000000 --- a/AI/Wrappers/JavaOO/src/main/java/com/springrts/ai/oo/AIEvent.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - Copyright (c) 2008 Robin Vobruba - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -*/ - -package com.springrts.ai.oo; - -/** - * An AI event is sent form the engine to Java Skirmish AIs. - * - * @author hoijui - * @version 0.1 - */ -public interface AIEvent { - -} diff --git a/AI/Wrappers/JavaOO/src/main/java/com/springrts/ai/oo/AIException.java b/AI/Wrappers/JavaOO/src/main/java/com/springrts/ai/oo/AIException.java deleted file mode 100644 index 2c3a9ddb927..00000000000 --- a/AI/Wrappers/JavaOO/src/main/java/com/springrts/ai/oo/AIException.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - Copyright (c) 2009 Robin Vobruba - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -*/ - -package com.springrts.ai.oo; - -/** - * Common base interface for AI related Exceptions. - * - * @author hoijui - * @version 0.1 - */ -public interface AIException { - - /** - * Returns the error associated with this Exception. - * This is used to send over low-level language interfaces, - * for example C, where exceptions are not supported. - * @return should be != 0, as this value is reserved for the no-error state - */ - public int getErrorNumber(); -} diff --git a/AI/Wrappers/JavaOO/src/main/java/com/springrts/ai/oo/AIFloat3.java b/AI/Wrappers/JavaOO/src/main/java/com/springrts/ai/oo/AIFloat3.java deleted file mode 100644 index 7865eac461c..00000000000 --- a/AI/Wrappers/JavaOO/src/main/java/com/springrts/ai/oo/AIFloat3.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - Copyright (c) 2008 Robin Vobruba - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -*/ - -package com.springrts.ai.oo; - - -import javax.vecmath.Tuple3d; -import javax.vecmath.Tuple3f; -import javax.vecmath.Vector3d; -import javax.vecmath.Vector3f; -import java.awt.Color; - -/** - * Represents a position on the map. - * - * @author hoijui.quaero@gmail.com - * @version 0.1 - */ -public class AIFloat3 extends Vector3f { - - public AIFloat3() { - super(0.0f, 0.0f, 0.0f); - } - public AIFloat3(float x, float y, float z) { - super(x, y, z); - } - public AIFloat3(float[] xyz) { - super(xyz); - } - public AIFloat3(AIFloat3 other) { - super(other); - } - public AIFloat3(Tuple3d tub3d) { - super(tub3d); - } - public AIFloat3(Tuple3f tub3f) { - super(tub3f); - } - public AIFloat3(Vector3d vec3d) { - super(vec3d); - } - public AIFloat3(Vector3f vec3f) { - super(vec3f); - } - public AIFloat3(Color color) { - - this.x = color.getRed() / 255.0F; - this.y = color.getGreen() / 255.0F; - this.z = color.getBlue() / 255.0F; - } - - public Color toColor() { - return new Color(x, y, z); - } - @Override - public String toString() { - return "(" + this.x + ", " + this.y + ", " + this.z + ")"; - } - public float[] toFloatArray() { - - float[] floatArr = new float[3]; - loadInto(floatArr); - return floatArr; - } - public void loadInto(float[] xyz) { - - xyz[0] = x; - xyz[1] = y; - xyz[2] = z; - } - - @Override - public int hashCode() { - - final int prime = 31; - int result = super.hashCode(); - result = prime * result + Float.floatToIntBits(x); - result = prime * result + Float.floatToIntBits(y); - result = prime * result + Float.floatToIntBits(z); - return result; - } - @Override - public boolean equals(Object obj) { - - if (this == obj) { - return true; - } else if (!super.equals(obj)) { - return false; - } else if (getClass() != obj.getClass()) { - return false; - } - - AIFloat3 other = (AIFloat3) obj; - if (Float.floatToIntBits(x) != Float.floatToIntBits(other.x)) { - return false; - } else if (Float.floatToIntBits(y) != Float.floatToIntBits(other.y)) { - return false; - } else if (Float.floatToIntBits(z) != Float.floatToIntBits(other.z)) { - return false; - } else { - return true; - } - } -} diff --git a/AI/Wrappers/JavaOO/src/main/java/com/springrts/ai/oo/CallbackAIException.java b/AI/Wrappers/JavaOO/src/main/java/com/springrts/ai/oo/CallbackAIException.java deleted file mode 100644 index 309663ef976..00000000000 --- a/AI/Wrappers/JavaOO/src/main/java/com/springrts/ai/oo/CallbackAIException.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - Copyright (c) 2009 Robin Vobruba - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -*/ - -package com.springrts.ai.oo; - -/** - * An exception of this type may be thrown while from an AI callback method. - * - * @author hoijui - */ -public class CallbackAIException extends RuntimeException implements AIException { - - private String methodName; - private int errorNumber; - - public CallbackAIException(String methodName, int errorNumber) { - super("Error calling method \"" + methodName + "\": " + errorNumber); - - this.methodName = methodName; - this.errorNumber = errorNumber; - } - public CallbackAIException(String methodName, int errorNumber, Throwable cause) { - super("Error calling method \"" + methodName + "\": " + errorNumber, cause); - - this.methodName = methodName; - this.errorNumber = errorNumber; - } - - /** - * Returns the name of the method in which the exception occurred. - */ - public String getMethodName() { - return methodName; - } - - /** - * Returns the error number that will be sent to the engine, - * and consequently appear in the engines main log file. - * @return should be != 0, as this value is reserved for the no-error state - */ - @Override - public int getErrorNumber() { - return errorNumber; - } -} diff --git a/AI/Wrappers/JavaOO/src/main/java/com/springrts/ai/oo/EventAIException.java b/AI/Wrappers/JavaOO/src/main/java/com/springrts/ai/oo/EventAIException.java deleted file mode 100644 index 8486a33f23f..00000000000 --- a/AI/Wrappers/JavaOO/src/main/java/com/springrts/ai/oo/EventAIException.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - Copyright (c) 2009 Robin Vobruba - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -*/ - -package com.springrts.ai.oo; - -/** - * An exception of this type may be thrown while handling an AI event. - * - * @author hoijui - * @version 0.1 - */ -public class EventAIException extends Exception implements AIException { - - public static final int DEFAULT_ERROR_NUMBER = 10; - - private int errorNumber; - - public EventAIException() { - super(); - - this.errorNumber = DEFAULT_ERROR_NUMBER; - } - public EventAIException(int errorNumber) { - super(); - - this.errorNumber = errorNumber; - } - - public EventAIException(String message) { - super(message); - - this.errorNumber = DEFAULT_ERROR_NUMBER; - } - public EventAIException(String message, int errorNumber) { - super(message); - - this.errorNumber = errorNumber; - } - - public EventAIException(String message, Throwable cause) { - super(message, cause); - - this.errorNumber = DEFAULT_ERROR_NUMBER; - } - public EventAIException(String message, Throwable cause, int errorNumber) { - super(message, cause); - - this.errorNumber = errorNumber; - } - - public EventAIException(Throwable cause) { - super(cause); - - this.errorNumber = DEFAULT_ERROR_NUMBER; - } - public EventAIException(Throwable cause, int errorNumber) { - super(cause); - - this.errorNumber = errorNumber; - } - - /** - * Returns the error number that will be sent to the engine, - * and consequently appear in the engines main log file. - * @return should be != 0, as this value is reserved for the no-error state - */ - @Override - public int getErrorNumber() { - return errorNumber; - } -} diff --git a/CMakeLists.txt b/CMakeLists.txt index d016d27287c..243ef8cb697 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -73,11 +73,6 @@ else() set(DEBUG_BUILD FALSE) endif() -if(DEBUG_BUILD) - set(JAVA_COMPILE_FLAG_CONDITIONAL "-g:lines,source,vars") -else() - set(JAVA_COMPILE_FLAG_CONDITIONAL "-g:lines,source") -endif() # By default, the libraries that don't explicitly specify SHARED/STATIC are build statically. # See https://cmake.org/cmake/help/latest/variable/BUILD_SHARED_LIBS.html @@ -211,7 +206,7 @@ endif (UNIX AND NOT MINGW) # (next two are relative to CMAKE_INSTALL_PREFIX) set(AI_LIBS_DIR "${DATADIR}" CACHE STRING "Where to install Skirmish AI libraries") set(AI_DATA_DIR "${AI_LIBS_DIR}" CACHE STRING "Where to install Skirmish AI additional files (eg. configuration)") -set(AI_TYPES "NATIVE" CACHE STRING "Which AI Interfaces (and Skirmish AIs using them) to build [ALL|NATIVE|JAVA|NONE]") +set(AI_TYPES "NATIVE" CACHE STRING "Which AI Interfaces (and Skirmish AIs using them) to build [ALL|NATIVE|NONE]") ## DataDirs set(BUILTIN_DATADIRS "") diff --git a/rts/build/cmake/UtilJava.cmake b/rts/build/cmake/UtilJava.cmake deleted file mode 100644 index 71874cc4966..00000000000 --- a/rts/build/cmake/UtilJava.cmake +++ /dev/null @@ -1,101 +0,0 @@ -# This file is part of the Spring engine (GPL v2 or later), see LICENSE.html - -# -# Spring Java related CMake utilities -# ----------------------------------- -# -# Variables set in this file: -# * JAVA_GLOBAL_LIBS_DIRS -# -# Functions and macros defined in this file: -# * find_java_lib -# * get_first_sub_dir_name -# * create_classpath -# * concat_classpaths -# * find_manifest_file -# - - -if (CMAKE_HOST_WIN32) - set(JAVA_GLOBAL_LIBS_DIRS "${MINGWLIBS}") -else (CMAKE_HOST_WIN32) - set(JAVA_GLOBAL_LIBS_DIRS "/usr/share/java" "/usr/local/share/java") -endif (CMAKE_HOST_WIN32) -make_global(JAVA_GLOBAL_LIBS_DIRS) - - -# Looks for a Java library (Jar file) in system wide search paths and in -# additional dirs supplied as argument. -macro (find_java_lib path_var libName additionalSearchDirs) - find_file(${path_var} "${libName}.jar" - PATHS ${additionalSearchDirs} ${JAVA_GLOBAL_LIBS_DIRS} - DOC "Path to the Java library ${libName}.jar" - NO_DEFAULT_PATH - NO_CMAKE_FIND_ROOT_PATH - ) - if (NOT ${path_var}) - message(SEND_ERROR "${libName}.jar not found!") - else (NOT ${path_var}) - ai_message(STATUS "Found ${libName}.jar: ${${path_var}}") - endif (NOT ${path_var}) -endmacro (find_java_lib path_var libName additionalSearchDirs) - - -# Returns the name of the first sub-dir (in alphabetical descending order) -# under dir. -macro (get_first_sub_dir_name name_var dir) - file(GLOB dirContent RELATIVE "${dir}" "${dir}/*") - foreach (dirPart ${dirContent}) - if (IS_DIRECTORY "${dir}/${dirPart}") - set(${name_var} ${dirPart}) - break() - endif (IS_DIRECTORY "${dir}/${dirPart}") - endforeach (dirPart) -endmacro (get_first_sub_dir_name name_var dir) - - -# Recursively lists all JAR files in a given directory -# and concatenates them in a Java Classpath compatible way into a single string. -macro (create_classpath classPath_var dir) - file(GLOB_RECURSE ${classPath_var} FOLLOW_SYMLINKS "${dir}/*.jar") - # Make sure we use the correct path delimiter for the compiling system - string(REPLACE ";" "${PATH_DELIM_H}" ${classPath_var} "${${classPath_var}}") -endmacro (create_classpath classPath_var dir) - - -# Concatenates an arbitrary number of Java ClassPaths (may be empty). -function (concat_classpaths resultingCP_var) - set(${resultingCP_var} "") - foreach (cpPart ${ARGN}) - set(${resultingCP_var} "${${resultingCP_var}}${cpPart}${PATH_DELIM_H}") - endforeach (cpPart) - string(REGEX REPLACE "${PATH_DELIM_H}\$" "" ${resultingCP_var} "${${resultingCP_var}}") - set(${resultingCP_var} "${${resultingCP_var}}" PARENT_SCOPE) -endfunction (concat_classpaths) - -# Look for a manifest.mf file in a few specific sub-dirs. -# This could be done with a simple find_file call, -# but that strangely does not find the file under win32, -# so we use this workaround -function (find_manifest_file srcDir result_var) - set(manifestSubdirs - "/src/main/resources/META-INF/" - "/src/" - "/") - set(${result_var} "${result_var}-NOTFOUND") - if (CMAKE_HOST_WIN32) - foreach(subDir_var ${manifestSubdirs}) - if (EXISTS "${srcDir}${subDir_var}manifest.mf") - set(${result_var} "${srcDir}${subDir_var}manifest.mf") - break() - endif (EXISTS "${srcDir}${subDir_var}manifest.mf") - endforeach(subDir_var) - else (CMAKE_HOST_WIN32) - find_file(${result_var} - NAMES "manifest.mf" "MANIFEST.MF" - PATHS "${srcDir}" - PATH_SUFFIXES ${manifestSubdirs} - NO_DEFAULT_PATH) - endif (CMAKE_HOST_WIN32) - set(${result_var} ${${result_var}} PARENT_SCOPE) -endfunction (find_manifest_file) From 9c2cb5f4ec2eafa5b4b633090d9bcf8e901d5b7a Mon Sep 17 00:00:00 2001 From: Robert Burnham Date: Fri, 10 Jul 2026 17:13:20 -0500 Subject: [PATCH 40/90] Fix unbindaction not clearing scancode bindings --- rts/Game/UI/KeyBindings.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/rts/Game/UI/KeyBindings.cpp b/rts/Game/UI/KeyBindings.cpp index 9bafd6ca542..4dd9e097110 100644 --- a/rts/Game/UI/KeyBindings.cpp +++ b/rts/Game/UI/KeyBindings.cpp @@ -740,7 +740,13 @@ bool CKeyBindings::UnBindAction(const std::string& command) RECOIL_DETAILED_TRACY_ZONE; if (debugEnabled) LOG("[CKeyBindings::%s] command=%s", __func__, command.c_str()); - return RemoveActionFromKeyMap(command, codeBindings) || RemoveActionFromKeyMap(command, scanBindings); + + // clear both maps; || would short-circuit and leave the scancode binding when + // the action is also bound to a keycode + const bool removedFromCode = RemoveActionFromKeyMap(command, codeBindings); + const bool removedFromScan = RemoveActionFromKeyMap(command, scanBindings); + + return removedFromCode || removedFromScan; } From 6c9781525213cbe39b4af8e0ed5b059e2e420f81 Mon Sep 17 00:00:00 2001 From: Bruno Da Silva Date: Mon, 13 Jul 2026 21:38:15 +0000 Subject: [PATCH 41/90] fix: bump pr-downloader to suppress false positive gcc warning --- tools/pr-downloader | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/pr-downloader b/tools/pr-downloader index 1b95b70bdb6..09a1f37c77a 160000 --- a/tools/pr-downloader +++ b/tools/pr-downloader @@ -1 +1 @@ -Subproject commit 1b95b70bdb63923f21f2b6b08240dd769cc733b9 +Subproject commit 09a1f37c77a014f24c3116ac7b7d2e318d0ce0de From 3b76f9778aba9f0ef8baa3f71851406d28ec0107 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Ptaszek?= Date: Tue, 14 Jul 2026 00:37:16 +0200 Subject: [PATCH 42/90] `Spring.SetSkyBoxTexture` performs setup if needed Previously a map had to have a skybox texture right from the start for it to be usable. Now sets it up at runtime. --- rts/Lua/LuaUnsyncedCtrl.cpp | 3 +-- rts/Rendering/Env/ISky.cpp | 24 ++++++++++++++++++++++++ rts/Rendering/Env/ISky.h | 1 + 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/rts/Lua/LuaUnsyncedCtrl.cpp b/rts/Lua/LuaUnsyncedCtrl.cpp index 8156e83f52f..2079d9724ce 100644 --- a/rts/Lua/LuaUnsyncedCtrl.cpp +++ b/rts/Lua/LuaUnsyncedCtrl.cpp @@ -2150,8 +2150,7 @@ int LuaUnsyncedCtrl::SetSkyBoxTexture(lua_State* L) if (CLuaHandle::GetHandleSynced(L)) return 0; - if (const auto& sky = ISky::GetSky(); sky != nullptr) - sky->SetLuaTexture(ParseLuaTextureData(L, false)); + ISky::SetSkyLuaTexture(ParseLuaTextureData(L, false)); return 0; } diff --git a/rts/Rendering/Env/ISky.cpp b/rts/Rendering/Env/ISky.cpp index 1e44ecabeb0..b662d8293e5 100644 --- a/rts/Rendering/Env/ISky.cpp +++ b/rts/Rendering/Env/ISky.cpp @@ -89,6 +89,30 @@ void ISky::SetSky() } } +void ISky::SetSkyLuaTexture(const MapTextureData& td) +{ + if (sky == nullptr) + return; + + /* TODO: consider if perhaps there should be some way to set the + * sky to one of the other classes (CModernSky, etc) via Lua. */ + + if (td.id != 0u && dynamic_cast(sky.get()) == nullptr) { + auto luaSky = std::make_unique(td.id, td.size.x, td.size.y); + + if (luaSky->IsValid()) { + sky = std::move(luaSky); + return; + } + + LOG_L(L_WARNING, "[ISky::%s] failed to create SkyBox from Lua texture (%u), keeping current sky", __func__, td.id); + return; + } + + /* FIXME: td.id == 0 reaches here. Untested in recent times */ + sky->SetLuaTexture(td); +} + void ISky::SetSkyAxisAngle(const float4& skyAxisAngleRaw) { auto axis = float3{ skyAxisAngleRaw.x, skyAxisAngleRaw.y, skyAxisAngleRaw.z }; diff --git a/rts/Rendering/Env/ISky.h b/rts/Rendering/Env/ISky.h index e0632e96d5d..62fc19d188c 100644 --- a/rts/Rendering/Env/ISky.h +++ b/rts/Rendering/Env/ISky.h @@ -50,6 +50,7 @@ class ISky void SetUpdated() { updated = true; } public: static void SetSky(); + static void SetSkyLuaTexture(const MapTextureData& td); static auto& GetSky() { return sky; } static void KillSky() { sky = nullptr; } public: From d306d399f9636aaf7ad516cc1979d7558ad98d81 Mon Sep 17 00:00:00 2001 From: Tom J Nowell Date: Sat, 4 Jul 2026 23:55:05 +0100 Subject: [PATCH 43/90] LuaParser: run Spring.TimeCheck's callback under unitsync/dedicated Under UNITSYNC and DEDICATED, TimeCheck compiled to `return 0`, silently dropping the function it was handed. The shipped gamedata/defs.lua wraps all unitdef/weapondef/etc. loading in Spring.TimeCheck(...), so ProcessUnits() ends up with an empty root and throws "root unitdef table invalid" - unitsync reports zero units for every game whose defs.lua uses TimeCheck (the standard springcontent path). Only the profiling timer needs to be excluded from those builds, not the call itself. --- rts/Lua/LuaParser.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/rts/Lua/LuaParser.cpp b/rts/Lua/LuaParser.cpp index 45c44ebf9e4..6ca8394fc43 100644 --- a/rts/Lua/LuaParser.cpp +++ b/rts/Lua/LuaParser.cpp @@ -501,12 +501,13 @@ void LuaParser::AddString(int key, const std::string& value) int LuaParser::TimeCheck(lua_State* L) { - #if (!defined(UNITSYNC) && !defined(DEDICATED)) if (!lua_isstring(L, 1) || !lua_isfunction(L, 2)) luaL_error(L, "Invalid arguments to TimeCheck('string', func, ...)"); { + #if (!defined(UNITSYNC) && !defined(DEDICATED)) ScopedOnceTimer timer(lua_tostring(L, 1)); + #endif lua_remove(L, 1); @@ -519,9 +520,6 @@ int LuaParser::TimeCheck(lua_State* L) } return lua_gettop(L); - #else - return 0; - #endif } From 598d8665445652253fa9601c6bec84b94efc0d4c Mon Sep 17 00:00:00 2001 From: TarnishedKnight Date: Tue, 14 Jul 2026 07:20:26 +0100 Subject: [PATCH 44/90] fix wide char to utf8 conversions. (#3100) * fix wide char to utf8 conversions. --- rts/System/Platform/Win/WinVersion.cpp | 36 ++++++++++++++++---------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/rts/System/Platform/Win/WinVersion.cpp b/rts/System/Platform/Win/WinVersion.cpp index d6ba5cf40dd..c90dd16ae54 100644 --- a/rts/System/Platform/Win/WinVersion.cpp +++ b/rts/System/Platform/Win/WinVersion.cpp @@ -1,5 +1,6 @@ /* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ +#include #include #include #include @@ -286,15 +287,13 @@ std::string windows::GetDisplayString(bool getName, bool getVersion, bool getExt oss << " (build " << osvi.dwBuildNumber; if (osvi.szCSDVersion[0] != 0) { - static_assert(sizeof(wchar_t) >= sizeof(osvi.szCSDVersion[0]), ""); + const int utf8Len = WideCharToMultiByte(CP_UTF8, 0, osvi.szCSDVersion, -1, nullptr, 0, nullptr, nullptr); - // Windows uses UTF16 - std::wstring_convert, wchar_t> ws2s; - - const std::wstring wstr(osvi.szCSDVersion); - const std::string nstr(ws2s.to_bytes(wstr)); - - oss << ", " << nstr; + if (utf8Len > 1) { + std::string nstr(utf8Len - 1, '\0'); + WideCharToMultiByte(CP_UTF8, 0, osvi.szCSDVersion, -1, nstr.data(), utf8Len, nullptr, nullptr); + oss << ", " << nstr; + } } oss << ")"; @@ -309,14 +308,25 @@ std::string windows::GetHardwareString() { std::ostringstream oss; - unsigned char regbuf[200]; - DWORD regLength = sizeof(regbuf); + std::array regbuf{}; + DWORD regLength = static_cast(regbuf.size() * sizeof(wchar_t)); // RegQueryValueExW wants bytes DWORD regType = REG_SZ; HKEY regkey; - if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, L"Hardware\\Description\\System\\CentralProcessor\\0", 0, KEY_READ, ®key) == ERROR_SUCCESS) { - if (RegQueryValueEx(regkey, L"ProcessorNameString", 0, ®Type, regbuf, ®Length) == ERROR_SUCCESS) { - oss << regbuf << "; "; + if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"Hardware\\Description\\System\\CentralProcessor\\0", 0, KEY_READ, ®key) == ERROR_SUCCESS) { + if (RegQueryValueExW(regkey, L"ProcessorNameString", 0, ®Type, reinterpret_cast(regbuf.data()), ®Length) == ERROR_SUCCESS) { + // REG_SZ values aren't guaranteed to be null-terminated; force it + regbuf.back() = L'\0'; + + const int utf8Len = WideCharToMultiByte(CP_UTF8, 0, regbuf.data(), -1, nullptr, 0, nullptr, nullptr); + + if (utf8Len > 1) { + std::string utf8str(utf8Len - 1, '\0'); + WideCharToMultiByte(CP_UTF8, 0, regbuf.data(), -1, utf8str.data(), utf8Len, nullptr, nullptr); + oss << utf8str << "; "; + } else { + oss << "cannot convert processor name to UTF-8; "; + } } else { oss << "cannot read processor data; "; } From 69f829dcdc9e79b844cab32873b0b3e85cd1cfcb Mon Sep 17 00:00:00 2001 From: Robert Burnham Date: Tue, 14 Jul 2026 20:11:17 -0500 Subject: [PATCH 45/90] Add `debug.emulateFoo` input emulation API (#3097) A bunch of functions to emulate key presses and mouse clicks added to the debug library, mostly for headless testing of UI. --- rts/Game/Game.cpp | 4 + rts/Game/UI/MouseHandler.cpp | 27 ++++ rts/Game/UI/MouseHandler.h | 10 ++ rts/Lua/CMakeLists.txt | 1 + rts/Lua/LuaDebugExtra.cpp | 240 ++++++++++++++++++++++++++++++++ rts/Lua/LuaDebugExtra.h | 27 ++++ rts/Lua/LuaHandleSynced.cpp | 2 + rts/Lua/LuaUI.cpp | 2 + rts/System/Input/KeyInput.cpp | 53 +++++++ rts/System/Input/KeyInput.h | 7 + rts/System/Input/MouseInput.cpp | 5 +- rts/System/SpringApp.cpp | 17 ++- 12 files changed, 391 insertions(+), 4 deletions(-) create mode 100644 rts/Lua/LuaDebugExtra.cpp create mode 100644 rts/Lua/LuaDebugExtra.h diff --git a/rts/Game/Game.cpp b/rts/Game/Game.cpp index 6da6bf2b229..5efed3cfd60 100644 --- a/rts/Game/Game.cpp +++ b/rts/Game/Game.cpp @@ -53,6 +53,7 @@ #include "Rendering/Textures/NamedTextures.h" #include "Lua/LuaGaia.h" #include "Lua/LuaHandle.h" +#include "Lua/LuaDebugExtra.h" #include "Lua/LuaInputReceiver.h" #include "Lua/LuaMenu.h" #include "Lua/LuaRules.h" @@ -1022,6 +1023,9 @@ void CGame::KillInterface() spring::SafeDelete(tooltip); // CTooltipConsole* LOG("[Game::%s][2]", __func__); + // drop emulated input state (game-scoped, like the bindings below); no-fire + // since the Lua handles are being destroyed + LuaDebugExtra::ClearEmulatedInput(false); keyBindings.Kill(); selectionKeys.Kill(); // CSelectionKeyHandler* spring::SafeDelete(inMapDrawerModel); diff --git a/rts/Game/UI/MouseHandler.cpp b/rts/Game/UI/MouseHandler.cpp index 60640294423..400cffa743f 100644 --- a/rts/Game/UI/MouseHandler.cpp +++ b/rts/Game/UI/MouseHandler.cpp @@ -305,6 +305,33 @@ void CMouseHandler::MouseMove(int x, int y, int dx, int dy) } +void CMouseHandler::SetButtonEmulated(int button, bool pressed) +{ + if (button < 1 || button > NUM_BUTTONS) + return; + + const bool physicalDown = (SDL_GetMouseState(nullptr, nullptr) & SDL_BUTTON(button)) != 0; + const bool wasDown = physicalDown || buttons[button].emulated; + + buttons[button].emulated = pressed; + + // fire only on an effective (physical-or-emulated) edge, so a real press + // underneath an emulated one doesn't produce a duplicate event + if (pressed && !wasDown) { + MousePress(lastx, lasty, button); + } else if (!pressed && wasDown && !physicalDown) { + MouseRelease(lastx, lasty, button); + } +} + + +void CMouseHandler::ClearEmulatedButtons() +{ + for (int button = 1; button <= NUM_BUTTONS; ++button) + buttons[button].emulated = false; +} + + void CMouseHandler::MousePress(int x, int y, int button) { RECOIL_DETAILED_TRACY_ZONE; diff --git a/rts/Game/UI/MouseHandler.h b/rts/Game/UI/MouseHandler.h index b36f4b7deb1..0b8074efdb6 100644 --- a/rts/Game/UI/MouseHandler.h +++ b/rts/Game/UI/MouseHandler.h @@ -52,6 +52,15 @@ class CMouseHandler void MousePress(int x, int y, int button); void MouseMove(int x, int y, int dx, int dy); void MouseWheel(float delta); + + // input emulation (debug.emulateMouse*): a button held independent of hardware. + // SetButtonEmulated fires the press/release itself, on the physical-or-emulated edge + void SetButtonEmulated(int button, bool pressed); + // drop all emulated flags without firing an event (game teardown) + void ClearEmulatedButtons(); + // bounds-guarded: the SDL-event gate calls this with a raw Uint8 button that + // can exceed NUM_BUTTONS on multi-button mice + bool IsButtonEmulated(int button) const { return (button >= 1 && button <= NUM_BUTTONS) && buttons[button].emulated; } void WindowLeave(); void WindowEnter(); @@ -153,6 +162,7 @@ class CMouseHandler struct ButtonPressEvt { bool pressed = false; + bool emulated = false; bool chorded = false; int x = 0; int y = 0; diff --git a/rts/Lua/CMakeLists.txt b/rts/Lua/CMakeLists.txt index 84bb4553c3d..b14d9f088aa 100644 --- a/rts/Lua/CMakeLists.txt +++ b/rts/Lua/CMakeLists.txt @@ -10,6 +10,7 @@ set(sources_engine_Lua "${CMAKE_CURRENT_SOURCE_DIR}/LuaConstEngine.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/LuaConstGame.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/LuaConstPlatform.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/LuaDebugExtra.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/LuaVFSDownload.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/LuaEncoding.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/LuaFBOs.cpp" diff --git a/rts/Lua/LuaDebugExtra.cpp b/rts/Lua/LuaDebugExtra.cpp new file mode 100644 index 00000000000..7b840cba4c3 --- /dev/null +++ b/rts/Lua/LuaDebugExtra.cpp @@ -0,0 +1,240 @@ +/* This file is part of the Recoil engine (GPL v2 or later), see LICENSE.html */ + +#include "LuaDebugExtra.h" + +#include "LuaInclude.h" +#include "LuaUtils.h" + +#include "Game/GameController.h" +#include "Game/UI/KeyBindings.h" +#include "Game/UI/KeyCodes.h" +#include "Game/UI/ScanCodes.h" +#include "Game/UI/MouseHandler.h" +#include "Rendering/GlobalRendering.h" +#include "System/Input/KeyInput.h" +#include "System/Input/MouseInput.h" +#include "System/Platform/SDL1_keysym.h" + +#include + +#include +#include +#include + + +/****************************************************************************** + * debug input emulation + * + * Callouts that feed input to the engine as if it came from real hardware, + * for headless regression tests. Emulated presses are held in a separate store + * and OR'd into the real input state; an event fires only when the combined + * (physical-or-emulated) state actually changes, so Lua never sees two Presses + * or two Releases in a row. + * + * No engine-side access gate: the property that must not regress (no doubled + * events) is structural, not access-controlled. A game that wants to restrict + * these nils them out. + * + * @see rts/Lua/LuaDebugExtra.cpp +******************************************************************************/ + +bool LuaDebugExtra::PushEntries(lua_State* L) +{ + LuaPushNamedCFunc(L, "emulateKeyPress", EmulateKeyPress); + LuaPushNamedCFunc(L, "emulateKeyRelease", EmulateKeyRelease); + LuaPushNamedCFunc(L, "emulateMousePress", EmulateMousePress); + LuaPushNamedCFunc(L, "emulateMouseRelease", EmulateMouseRelease); + LuaPushNamedCFunc(L, "emulateMouseMove", EmulateMouseMove); + LuaPushNamedCFunc(L, "clearEmulatedInput", ClearEmulatedInputLua); + + return true; +} + + +// shared body for emulateKeyPress/emulateKeyRelease; only the store update and +// the edge-fire differ, keyed on `pressed` +static int emulateKey(lua_State* L, bool pressed) +{ + if (activeController == nullptr) + return 0; + + // Lua passes SDL1.2 keysyms; the held-state side (keyVec/IsKeyPressed) works in + // raw SDL2, while the event side wants the normalized code like a real KEYDOWN + const int rawKey = SDL12_keysyms(luaL_checkint(L, 1)); + + // reject a junk keycode (unmapped -> SDLK_UNKNOWN). We deliberately do NOT + // reject on an unknown scancode: headless has no keyboard layout, so + // SDL_GetScancodeFromKey returns SDL_SCANCODE_UNKNOWN even for valid keys + if (rawKey == SDLK_UNKNOWN) + return 0; + + const SDL_Scancode sc = SDL_GetScancodeFromKey((SDL_Keycode)rawKey); + const int eventKey = CKeyCodes::GetNormalizedSymbol(rawKey); + const int scanCode = CScanCodes::GetNormalizedSymbol(sc); + + int numKeys = 0; + const uint8_t* kbState = SDL_GetKeyboardState(&numKeys); + const bool physicalDown = ((int)sc < numKeys && kbState[sc] != 0); + + // effective (physical-or-emulated) state before this call + const bool wasDown = physicalDown || KeyInput::IsKeyEmulated(rawKey); + + KeyInput::SetKeyEmulated(rawKey, pressed); + KeyInput::Update(keyBindings.GetFakeMetaKey()); + + if (pressed) { + // fire only on a false->true edge + if (!wasDown) + activeController->KeyPressed(eventKey, scanCode, false); + } else { + // effective after = physical; fire only on a true->false edge + if (wasDown && !physicalDown) + activeController->KeyReleased(eventKey, scanCode); + } + + return 0; +} + + +/*** Emulate a keyboard key being pressed and held. + * + * Fires the KeyPress event and holds the key down (merged with real hardware + * state) until released or cleared. The accompanying scancode is derived from + * the keycode using the currently active system keyboard layout. + * + * @function debug.emulateKeyPress + * @param keycode integer + * @return nil + */ +int LuaDebugExtra::EmulateKeyPress(lua_State* L) { return emulateKey(L, true); } + + +/*** Emulate a held keyboard key being released. + * + * @function debug.emulateKeyRelease + * @param keycode integer + * @return nil + */ +int LuaDebugExtra::EmulateKeyRelease(lua_State* L) { return emulateKey(L, false); } + + +/*** Emulate a mouse button being pressed and held. + * + * @function debug.emulateMousePress + * @param button integer + * @return nil + */ +int LuaDebugExtra::EmulateMousePress(lua_State* L) +{ + if (mouse == nullptr) + return 0; + + const int button = luaL_checkint(L, 1); + + if (button < 1 || button > NUM_BUTTONS) + return 0; + + mouse->SetButtonEmulated(button, true); + return 0; +} + + +/*** Emulate a held mouse button being released. + * + * @function debug.emulateMouseRelease + * @param button integer + * @return nil + */ +int LuaDebugExtra::EmulateMouseRelease(lua_State* L) +{ + if (mouse == nullptr) + return 0; + + const int button = luaL_checkint(L, 1); + + if (button < 1 || button > NUM_BUTTONS) + return 0; + + mouse->SetButtonEmulated(button, false); + return 0; +} + + +/*** Emulate the cursor moving to a screen position. + * + * Fires a MouseMove through the normal pipeline. Coordinates use the bottom-left + * origin like the rest of the Lua screen API. Does not move the OS cursor. + * + * @function debug.emulateMouseMove + * @param x integer + * @param y integer + * @return nil + */ +int LuaDebugExtra::EmulateMouseMove(lua_State* L) +{ + if (mouse == nullptr || mouseInput == nullptr) + return 0; + + const int x = luaL_checkint(L, 1); + const int y = globalRendering->viewSizeY - luaL_checkint(L, 2) - 1; + + const int2 prev = mouseInput->GetPos(); + mouseInput->SetPos(int2(x, y)); + mouse->MouseMove(x, y, x - prev.x, y - prev.y); + + return 0; +} + + +/*** Release everything currently held via emulation. + * + * @function debug.clearEmulatedInput + * @return nil + */ +int LuaDebugExtra::ClearEmulatedInputLua(lua_State* L) +{ + ClearEmulatedInput(); + return 0; +} + + +void LuaDebugExtra::ClearEmulatedInput(bool fireReleases) +{ + // snapshot the emulated keys before clearing, so a fired release can't walk + // the store we are emptying + const std::set keyCodes = KeyInput::GetEmulatedKeys(); + + KeyInput::ClearEmulatedKeys(); + KeyInput::Update(keyBindings.GetFakeMetaKey()); + + if (fireReleases && activeController != nullptr) { + int numKeys = 0; + const uint8_t* kbState = SDL_GetKeyboardState(&numKeys); + + // the store holds raw SDL2 keycodes; the event side wants the normalized code + for (const int rawKey: keyCodes) { + const SDL_Scancode sc = SDL_GetScancodeFromKey((SDL_Keycode)rawKey); + + if ((int)sc < numKeys && kbState[sc] != 0) + continue; + + activeController->KeyReleased(CKeyCodes::GetNormalizedSymbol(rawKey), CScanCodes::GetNormalizedSymbol(sc)); + } + } + + if (mouse == nullptr) + return; + + // no-fire path (game teardown): drop the flags without dispatching into + // handles that are already being destroyed + if (!fireReleases) { + mouse->ClearEmulatedButtons(); + return; + } + + // SetButtonEmulated fires the release itself if the button ends up effectively up + for (int button = 1; button <= NUM_BUTTONS; ++button) { + if (mouse->IsButtonEmulated(button)) + mouse->SetButtonEmulated(button, false); + } +} diff --git a/rts/Lua/LuaDebugExtra.h b/rts/Lua/LuaDebugExtra.h new file mode 100644 index 00000000000..35d3c6dba09 --- /dev/null +++ b/rts/Lua/LuaDebugExtra.h @@ -0,0 +1,27 @@ +/* This file is part of the Recoil engine (GPL v2 or later), see LICENSE.html */ + +#ifndef LUA_DEBUG_EXTRA_H +#define LUA_DEBUG_EXTRA_H + +struct lua_State; + +class LuaDebugExtra { + public: + static bool PushEntries(lua_State* L); + + // drops all emulated key/button state; with fireReleases (default) it also + // dispatches balancing KeyReleased/MouseRelease. Shared by + // debug.clearEmulatedInput, the focus-loss handler (fire), and game teardown + // (no fire - the handles are being destroyed). + static void ClearEmulatedInput(bool fireReleases = true); + + private: + static int EmulateKeyPress(lua_State* L); + static int EmulateKeyRelease(lua_State* L); + static int EmulateMousePress(lua_State* L); + static int EmulateMouseRelease(lua_State* L); + static int EmulateMouseMove(lua_State* L); + static int ClearEmulatedInputLua(lua_State* L); +}; + +#endif /* LUA_DEBUG_EXTRA_H */ diff --git a/rts/Lua/LuaHandleSynced.cpp b/rts/Lua/LuaHandleSynced.cpp index 85851f3bc3c..180c8faae42 100644 --- a/rts/Lua/LuaHandleSynced.cpp +++ b/rts/Lua/LuaHandleSynced.cpp @@ -22,6 +22,7 @@ #include "LuaSyncedTable.h" #include "LuaUICommand.h" #include "LuaUnsyncedCtrl.h" +#include "LuaDebugExtra.h" #include "LuaUnsyncedRead.h" #include "LuaFeatureDefs.h" #include "LuaUnitDefs.h" @@ -130,6 +131,7 @@ bool CUnsyncedLuaHandle::Init(std::string code, const std::string& file) if (!AddEntriesToTable(L, "Spring", LuaUnsyncedCtrl::PushEntries )) KILL if (!AddEntriesToTable(L, "Spring", LuaUnsyncedRead::PushEntries )) KILL if (!AddEntriesToTable(L, "Spring", LuaUICommand::PushEntries )) KILL + if (!AddEntriesToTable(L, "debug", LuaDebugExtra::PushEntries )) KILL if (!AddEntriesToTable(L, "gl", LuaOpenGL::PushEntries )) KILL if (!AddEntriesToTable(L, "GL", LuaConstGL::PushEntries )) KILL if (!AddEntriesToTable(L, "Engine", LuaConstEngine::PushEntries )) KILL diff --git a/rts/Lua/LuaUI.cpp b/rts/Lua/LuaUI.cpp index 4b768f39f53..46f2bee87c6 100644 --- a/rts/Lua/LuaUI.cpp +++ b/rts/Lua/LuaUI.cpp @@ -13,6 +13,7 @@ #include "LuaConstEngine.h" #include "LuaConstGame.h" #include "LuaConstPlatform.h" +#include "LuaDebugExtra.h" #include "LuaSyncedRead.h" #include "LuaInterCall.h" #include "LuaLibs.h" @@ -128,6 +129,7 @@ CLuaUI::CLuaUI() !AddEntriesToTable(L, "Spring", LuaUnsyncedCtrl::PushEntries) || !AddEntriesToTable(L, "Spring", LuaUnsyncedRead::PushEntries) || !AddEntriesToTable(L, "Spring", LuaUICommand::PushEntries) || + !AddEntriesToTable(L, "debug", LuaDebugExtra::PushEntries) || !AddEntriesToTable(L, "gl", LuaOpenGL::PushEntries) || !AddEntriesToTable(L, "GL", LuaConstGL::PushEntries) || !AddEntriesToTable(L, "Engine", LuaConstEngine::PushEntries) || diff --git a/rts/System/Input/KeyInput.cpp b/rts/System/Input/KeyInput.cpp index ea46846863e..c7599558340 100644 --- a/rts/System/Input/KeyInput.cpp +++ b/rts/System/Input/KeyInput.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -24,6 +25,10 @@ namespace KeyInput { static SDL_Keymod keyMods; + // keycodes forced down by debug.emulateKey*; re-applied on top of the SDL + // poll at the end of every Update so they survive the re-poll + static std::set emulatedKeyCodes; + bool IsKeyPressed(int keyCode) { const auto& pred = keyCmp; @@ -61,6 +66,19 @@ namespace KeyInput { iter->second = isPressed; } + // mark a code pressed, inserting it (keeping the vector sorted) if the poll + // never created a slot for it -- headless has no SDL keyboard, so keyVec/scanVec + // come back empty and plain SetKeyPressed would have nothing to flip + static void ForcePressed(std::vector& vec, int code) { + const auto iter = std::lower_bound(vec.begin(), vec.end(), Key{code, false}, keyCmp); + + if (iter != vec.end() && iter->first == code) { + iter->second = true; + } else { + vec.insert(iter, Key{code, true}); + } + } + void SetKeyModState(int mod, bool isPressed) { if (isPressed) { keyMods = SDL_Keymod(keyMods | mod); @@ -73,6 +91,26 @@ namespace KeyInput { return (keyMods & mod); } + bool IsKeyEmulated(int keyCode) { + return emulatedKeyCodes.contains(keyCode); + } + + void SetKeyEmulated(int keyCode, bool pressed) { + if (pressed) { + emulatedKeyCodes.insert(keyCode); + } else { + emulatedKeyCodes.erase(keyCode); + } + } + + const std::set& GetEmulatedKeys() { + return emulatedKeyCodes; + } + + void ClearEmulatedKeys() { + emulatedKeyCodes.clear(); + } + /** * Tests SDL keystates and sets values in key array */ @@ -108,6 +146,21 @@ namespace KeyInput { SetKeyPressed(SDL_SCANCODE_LCTRL , GetKeyModState(KMOD_CTRL )); SetKeyPressed(SDL_SCANCODE_LGUI , GetKeyModState(KMOD_GUI )); SetKeyPressed(SDL_SCANCODE_LSHIFT, GetKeyModState(KMOD_SHIFT)); + + // OR the emulated keys back in: the poll above only reflects real hardware, + // so anything held via debug.emulateKey* has to be re-applied here to show + // up in IsKeyPressed / GetKeyModState / GetPressedKeys + for (const int keyCode: emulatedKeyCodes) { + ForcePressed(keyVec, keyCode); + ForcePressed(scanVec, SDL_GetScancodeFromKey((SDL_Keycode)keyCode)); + + switch (keyCode) { + case SDLK_LALT: case SDLK_RALT: SetKeyModState(KMOD_ALT , true); break; + case SDLK_LCTRL: case SDLK_RCTRL: SetKeyModState(KMOD_CTRL , true); break; + case SDLK_LGUI: case SDLK_RGUI: SetKeyModState(KMOD_GUI , true); break; + case SDLK_LSHIFT: case SDLK_RSHIFT: SetKeyModState(KMOD_SHIFT, true); break; + } + } } const std::vector& GetPressedKeys() diff --git a/rts/System/Input/KeyInput.h b/rts/System/Input/KeyInput.h index 980192a56bb..e493cdcdf93 100644 --- a/rts/System/Input/KeyInput.h +++ b/rts/System/Input/KeyInput.h @@ -4,6 +4,7 @@ #define KEYBOARD_INPUT_H #include +#include namespace KeyInput { void Update(int fakeMetaKey); @@ -14,6 +15,12 @@ namespace KeyInput { void SetKeyModState(int mod, bool pressed); bool GetKeyModState(int mod); + // input emulation (debug.emulateKey*): keys forced down independent of hardware + bool IsKeyEmulated(int keyCode); + void SetKeyEmulated(int keyCode, bool pressed); + const std::set& GetEmulatedKeys(); + void ClearEmulatedKeys(); + typedef std::pair Key; const std::vector& GetPressedKeys(); diff --git a/rts/System/Input/MouseInput.cpp b/rts/System/Input/MouseInput.cpp index c7fbed6708c..c76f45ac146 100644 --- a/rts/System/Input/MouseInput.cpp +++ b/rts/System/Input/MouseInput.cpp @@ -75,14 +75,15 @@ bool IMouseInput::HandleSDLMouseEvent(const SDL_Event& event) case SDL_MOUSEBUTTONDOWN: { mousepos = int2(event.button.x, event.button.y); - if (mouse != nullptr) + // suppress if the button is already held via input emulation + if (mouse != nullptr && !mouse->IsButtonEmulated(event.button.button)) mouse->MousePress(mousepos.x, mousepos.y, event.button.button); } break; case SDL_MOUSEBUTTONUP: { mousepos = int2(event.button.x, event.button.y); - if (mouse != nullptr) + if (mouse != nullptr && !mouse->IsButtonEmulated(event.button.button)) mouse->MouseRelease(mousepos.x, mousepos.y, event.button.button); } break; diff --git a/rts/System/SpringApp.cpp b/rts/System/SpringApp.cpp index a2274b6f9e5..cacaf260b78 100644 --- a/rts/System/SpringApp.cpp +++ b/rts/System/SpringApp.cpp @@ -43,6 +43,7 @@ #include "Game/UI/InfoConsole.h" #include "Game/UI/MouseHandler.h" #include "Lua/LuaOpenGL.h" +#include "Lua/LuaDebugExtra.h" #include "Lua/LuaVFSDownload.h" #include "Menu/LuaMenuController.h" #include "Menu/SelectMenu.h" @@ -1178,6 +1179,10 @@ bool SpringApp::MainEventHandler(const SDL_Event& event) //FIXME check if still happens with SDL2 (2013) SDL_SetModState((SDL_Keymod)(SDL_GetModState() & (KMOD_NUM | KMOD_CAPS | KMOD_MODE))); + // drop emulated input first: it fires its own releases directly, + // since the pushed SDL releases below get eaten by the emulation gate + LuaDebugExtra::ClearEmulatedInput(); + // release all keyboard keys KeyInput::ReleaseAllKeys(); @@ -1244,7 +1249,12 @@ bool SpringApp::MainEventHandler(const SDL_Event& event) if (activeController != nullptr) { int keyCode = CKeyCodes::GetNormalizedSymbol(event.key.keysym.sym); int scanCode = CScanCodes::GetNormalizedSymbol(event.key.keysym.scancode); - activeController->KeyPressed(keyCode, scanCode, event.key.repeat); + + // if the key is already held via input emulation the effective + // state is already down, so the real press is not a new edge + // (the emulated store is keyed by raw SDL2 keycode, like keyVec) + if (!KeyInput::IsKeyEmulated(event.key.keysym.sym)) + activeController->KeyPressed(keyCode, scanCode, event.key.repeat); } } break; @@ -1255,7 +1265,10 @@ bool SpringApp::MainEventHandler(const SDL_Event& event) gameTextInput.ignoreNextChar = false; int keyCode = CKeyCodes::GetNormalizedSymbol(event.key.keysym.sym); int scanCode = CScanCodes::GetNormalizedSymbol(event.key.keysym.scancode); - activeController->KeyReleased(keyCode, scanCode); + + // emulation still holds it down, so the real release is not an edge + if (!KeyInput::IsKeyEmulated(event.key.keysym.sym)) + activeController->KeyReleased(keyCode, scanCode); } } break; case SDL_KEYMAPCHANGED: { From 1a03e7482942d8c76a0d8f6ad4aa1670cf09c178 Mon Sep 17 00:00:00 2001 From: rlcevg <514675+rlcevg@users.noreply.github.com> Date: Wed, 15 Jul 2026 13:09:18 +0300 Subject: [PATCH 46/90] AIFloat3: Remove non-trivial copy constructor When `AIFloat3` had virtual methods - it required non-trivial copy constructor. It is a leftover that spoils AngelScript bindings to `float3` math methods that return `float3` as result. Insinuations: while AngelScript expects copying through registers (`this == nullptr`), non-trivial copy constructor makes it in memory trying to dereference nullptr. This is the 1st part of planned `AIFloat3` refactor. In 2nd part (as separate PR) `AIFloat3` will become a copy of `float3` interface and implementation with inheritance removed. --- AI/Wrappers/Cpp/src/AIFloat3.cpp | 4 ---- AI/Wrappers/Cpp/src/AIFloat3.h | 3 ++- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/AI/Wrappers/Cpp/src/AIFloat3.cpp b/AI/Wrappers/Cpp/src/AIFloat3.cpp index 83ffa7c2ed4..03c903c1841 100644 --- a/AI/Wrappers/Cpp/src/AIFloat3.cpp +++ b/AI/Wrappers/Cpp/src/AIFloat3.cpp @@ -16,10 +16,6 @@ springai::AIFloat3::AIFloat3(float* xyz) : float3(xyz) { } -springai::AIFloat3::AIFloat3(const springai::AIFloat3& other) - : float3(other) -{ -} springai::AIFloat3::AIFloat3(const float3& f3) : float3(f3) { diff --git a/AI/Wrappers/Cpp/src/AIFloat3.h b/AI/Wrappers/Cpp/src/AIFloat3.h index 6e00536a234..1c3ba587e3b 100644 --- a/AI/Wrappers/Cpp/src/AIFloat3.h +++ b/AI/Wrappers/Cpp/src/AIFloat3.h @@ -18,7 +18,8 @@ class AIFloat3 : public float3 { AIFloat3(); AIFloat3(float x, float y, float z); AIFloat3(float* xyz); - AIFloat3(const AIFloat3& other); + // must be trivial - same as float3, otherwise it ruins bindings + AIFloat3(const AIFloat3& other) = default; AIFloat3(const float3& f3); void LoadInto(float* xyz) const; From 8ee4aa090b9280db593e41d35f73f9b3cefd6e2f Mon Sep 17 00:00:00 2001 From: Tom J Nowell Date: Wed, 15 Jul 2026 18:09:36 +0100 Subject: [PATCH 47/90] macOS: symbolicate arm64 crashes with `atos -arch arm64` (#3064) The macOS crash handler hardcoded '-arch x86_64' when invoking atos (ADDR2LINE), so backtrace symbolication produced no useful output on Apple Silicon binaries. Select '-arch arm64' at compile time on aarch64/arm64 targets, keeping x86_64 for Intel. Also fix the debug log of the frame address: stackFrame.ip is a void*, but it was printed with '0x%lx', a format/argument mismatch. Use %p. --- rts/System/Platform/Mac/CrashHandler.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/rts/System/Platform/Mac/CrashHandler.cpp b/rts/System/Platform/Mac/CrashHandler.cpp index 5716add1092..0705b63fca4 100644 --- a/rts/System/Platform/Mac/CrashHandler.cpp +++ b/rts/System/Platform/Mac/CrashHandler.cpp @@ -73,7 +73,7 @@ static void TranslateStackTrace(StackTrace& stacktrace, const int logLevel) stackFrame.path = ""; } - LOG_L(L_DEBUG, "\tsymbol = \"%s\", path = \"%s\", addr = 0x%lx", stackFrame.symbol.c_str(), path, stackFrame.ip); + LOG_L(L_DEBUG, "\tsymbol = \"%s\", path = \"%s\", addr = %p", stackFrame.symbol.c_str(), path, stackFrame.ip); } LOG_L(L_DEBUG, "[%s][2]", __func__); @@ -116,7 +116,14 @@ static void TranslateStackTrace(StackTrace& stacktrace, const int logLevel) execCommandString.clear(); stackFrameIndices.clear(); - execCommandBuffer << ADDR2LINE << " -o " << modulePath << " -arch x86_64 -l " << std::hex << addrPathPair.first; + #if defined(__x86_64__) || defined(__amd64__) || defined(_M_X64) || defined(_M_AMD64) + constexpr const char* atosArch = "x86_64"; + #elif defined(__aarch64__) || defined(_M_ARM64) + constexpr const char* atosArch = "arm64"; + #else + #error "Unsupported architecture" + #endif + execCommandBuffer << ADDR2LINE << " -o " << modulePath << " -arch " << atosArch << " -l " << std::hex << addrPathPair.first; // insert requested addresses that should be translated by atos int i = 0; From 3f7e6497b04dd82b145d8a103fb198e4e2b7ab89 Mon Sep 17 00:00:00 2001 From: sprunk Date: Fri, 15 Aug 2025 03:52:51 +0200 Subject: [PATCH 48/90] Add `gadget:ResourceExcess(excessTable) -> bool` Passes a table: { [teamID] = {metal, energy, ...} } The resources are the amount over the storage max, accumulated over the frame. Called at the end of every frame, contains data for every team. Return true to take over engine handling; otherwise the native behaviour of buffering it until the next slowupdate will take place. --- rts/Lua/LuaHandleSynced.cpp | 37 ++++++++++++++++++++++++++++++++++++ rts/Lua/LuaHandleSynced.h | 2 ++ rts/Sim/Misc/Team.cpp | 7 ++++--- rts/Sim/Misc/Team.h | 1 + rts/Sim/Misc/TeamHandler.cpp | 27 ++++++++++++++++++++++++++ rts/Sim/Misc/TeamHandler.h | 1 + rts/System/EventClient.h | 4 ++++ rts/System/EventHandler.cpp | 6 ++++++ rts/System/EventHandler.h | 2 ++ rts/System/Events.def | 4 ++++ 10 files changed, 88 insertions(+), 3 deletions(-) diff --git a/rts/Lua/LuaHandleSynced.cpp b/rts/Lua/LuaHandleSynced.cpp index 180c8faae42..9ae6a0fe2b8 100644 --- a/rts/Lua/LuaHandleSynced.cpp +++ b/rts/Lua/LuaHandleSynced.cpp @@ -55,6 +55,7 @@ #include "System/Misc/TracyDefs.h" +#include LuaRulesParams::Params CSplitLuaHandle::gameParams; @@ -1198,6 +1199,42 @@ bool CSyncedLuaHandle::AllowResourceTransfer(int oldTeam, int newTeam, const cha return allow; } +/*** Called when excess resources are added. + * Accumulates all excesses within a single gameframe. + * + * @function SyncedCallins:ResourceExcess + * @param excesses table + * @return boolean whether or not Lua handled the event + */ +bool CSyncedLuaHandle::ResourceExcess(const std::map & excesses) +{ + RECOIL_DETAILED_TRACY_ZONE; + LUA_CALL_IN_CHECK(L, true); + luaL_checkstack(L, 3, __func__); + + static const LuaHashString cmdStr(__func__); + if (!cmdStr.GetGlobalFunc(L)) + return false; + + lua_createtable(L, excesses.size(), 1); + + for (const auto &[teamID, excess] : excesses) { + lua_createtable(L, excess.MAX_RESOURCES, 0); + for (const auto &[resourceID, resource] : std::views::enumerate(excess)) { + lua_pushnumber(L, resource); + lua_rawseti(L, -2, resourceID + 1); + } + lua_rawseti(L, -2, teamID); + } + + if (!RunCallIn(L, cmdStr, 1, 1)) + return false; + + const bool handled = luaL_optboolean(L, -1, false); + lua_pop(L, 1); + return handled; +} + /*** Determines if this unit can be controlled directly in FPS view. * diff --git a/rts/Lua/LuaHandleSynced.h b/rts/Lua/LuaHandleSynced.h index 5769543daa1..6acda8aa859 100644 --- a/rts/Lua/LuaHandleSynced.h +++ b/rts/Lua/LuaHandleSynced.h @@ -57,6 +57,8 @@ class CSyncedLuaHandle : public CLuaHandle bool CommandFallback(const CUnit* unit, const Command& cmd) override; bool AllowCommand(const CUnit* unit, const Command& cmd, int playerNum, bool fromSynced, bool fromLua) override; + bool ResourceExcess(const std::map & excess) override; + std::pair AllowUnitCreation(const UnitDef* unitDef, const CUnit* builder, const BuildInfo* buildInfo) override; bool AllowUnitTransfer(const CUnit* unit, int newTeam, bool capture) override; bool AllowUnitBuildStep(const CUnit* builder, const CUnit* unit, float part) override; diff --git a/rts/Sim/Misc/Team.cpp b/rts/Sim/Misc/Team.cpp index 1c89bb3ec34..2486a94ed7e 100644 --- a/rts/Sim/Misc/Team.cpp +++ b/rts/Sim/Misc/Team.cpp @@ -45,6 +45,7 @@ CR_REG_METADATA(CTeam, ( CR_MEMBER(resPrevExpense), CR_MEMBER(resShare), CR_MEMBER(resDelayedShare), + CR_MEMBER(resExcessThisFrame), CR_MEMBER(resSent), CR_MEMBER(resPrevSent), CR_MEMBER(resReceived), @@ -156,7 +157,7 @@ void CTeam::AddMetal(float amount, bool useIncomeMultiplier) if (res.metal <= resStorage.metal) return; - resDelayedShare.metal += (res.metal - resStorage.metal); + resExcessThisFrame.metal += (res.metal - resStorage.metal); res.metal = resStorage.metal; } @@ -170,7 +171,7 @@ void CTeam::AddEnergy(float amount, bool useIncomeMultiplier) resIncome.energy += amount; if (res.energy > resStorage.energy) { - resDelayedShare.energy += (res.energy - resStorage.energy); + resExcessThisFrame.energy += (res.energy - resStorage.energy); res.energy = resStorage.energy; } } @@ -196,7 +197,7 @@ void CTeam::AddResources(SResourcePack amount, bool useIncomeMultiplier) if (res[i] <= resStorage[i]) continue; - resDelayedShare[i] += (res[i] - resStorage[i]); + resExcessThisFrame[i] += (res[i] - resStorage[i]); res[i] = resStorage[i]; } } diff --git a/rts/Sim/Misc/Team.h b/rts/Sim/Misc/Team.h index 6bc74267dc9..3a1c0fad8a2 100644 --- a/rts/Sim/Misc/Team.h +++ b/rts/Sim/Misc/Team.h @@ -88,6 +88,7 @@ class CTeam : public TeamBase SResourcePack resIncome, resPrevIncome; SResourcePack resExpense, resPrevExpense; SResourcePack resShare; + SResourcePack resExcessThisFrame; //< accumulates excess over a gameframe SResourcePack resDelayedShare; //< excess that might be shared next SlowUpdate SResourcePack resSent, resPrevSent; SResourcePack resReceived, resPrevReceived; diff --git a/rts/Sim/Misc/TeamHandler.cpp b/rts/Sim/Misc/TeamHandler.cpp index d8b5d4e5afc..f582e9f4b2a 100644 --- a/rts/Sim/Misc/TeamHandler.cpp +++ b/rts/Sim/Misc/TeamHandler.cpp @@ -7,6 +7,7 @@ #include "Game/GameSetup.h" #include "Sim/Misc/GlobalConstants.h" #include "Sim/Misc/GlobalSynced.h" +#include "System/EventHandler.h" #include "System/Misc/TracyDefs.h" @@ -113,9 +114,35 @@ void CTeamHandler::SetDefaultStartPositions(const CGameSetup* setup) } } +void CTeamHandler::HandleFrameExcess() +{ + std::map excesses; + for (const auto &team : teams) + excesses.emplace(team.teamNum, team.resExcessThisFrame); + + /* Note that `resDelayedShare` is a metaaccumulator, + * the reason to have this two-layer accumulation is + * that handling excess right when it happens would + * be too expensive (for example you can have tens of + * thousands of windgens each generating a resource + * instance), having the Lua event handled at slow + * update would reduce control, and having the engine + * handle excess natively outside slow update would + * be inconsistent with other native resource handling. */ + if (!eventHandler.ResourceExcess(excesses)) + for (auto &team : teams) + team.resDelayedShare += team.resExcessThisFrame; + + for (auto &team : teams) + team.resExcessThisFrame = 0.0f; +} + void CTeamHandler::GameFrame(int frameNum) { RECOIL_DETAILED_TRACY_ZONE; + + HandleFrameExcess(); + if ((frameNum % TEAM_SLOWUPDATE_RATE) != 0) return; diff --git a/rts/Sim/Misc/TeamHandler.h b/rts/Sim/Misc/TeamHandler.h index 2e0e2aa7a2b..da7499d6581 100644 --- a/rts/Sim/Misc/TeamHandler.h +++ b/rts/Sim/Misc/TeamHandler.h @@ -161,6 +161,7 @@ class CTeamHandler bool TransferTeamMaxUnits(CTeam* fromTeam, CTeam* toTeam, int transferAmnt); private: + void HandleFrameExcess(); /** * @brief gaia team diff --git a/rts/System/EventClient.h b/rts/System/EventClient.h index 3e17d1171c2..6d938cebd5a 100644 --- a/rts/System/EventClient.h +++ b/rts/System/EventClient.h @@ -4,6 +4,7 @@ #define EVENT_CLIENT_H #include +#include #include #include #include @@ -35,6 +36,7 @@ struct BuildInfo; struct FeatureDef; class LuaMaterial; struct WeaponDef; +struct SResourcePack; #ifndef zipFile // might be defined through zip.h already @@ -122,6 +124,8 @@ class CEventClient virtual void TeamDied(int teamID) {} virtual void TeamChanged(int teamID) {} + virtual bool ResourceExcess(const std::map & excess) { return false; } + virtual void PlayerChanged(int playerID) {} virtual void PlayerAdded(int playerID) {} virtual void PlayerRemoved(int playerID, int reason) {} diff --git a/rts/System/EventHandler.cpp b/rts/System/EventHandler.cpp index d7969be7aa7..f815ae7f33e 100644 --- a/rts/System/EventHandler.cpp +++ b/rts/System/EventHandler.cpp @@ -581,6 +581,12 @@ void CEventHandler::TeamDied(int teamID) ITERATE_EVENTCLIENTLIST(TeamDied, teamID); } +bool CEventHandler::ResourceExcess(const std::map &excess) +{ + ZoneScoped; + return ControlIterateDefFalse(listResourceExcess, &CEventClient::ResourceExcess, excess); +} + void CEventHandler::TeamChanged(int teamID) { ZoneScoped; diff --git a/rts/System/EventHandler.h b/rts/System/EventHandler.h index e7432aaaf50..31d77790597 100644 --- a/rts/System/EventHandler.h +++ b/rts/System/EventHandler.h @@ -60,6 +60,8 @@ class CEventHandler void TeamDied(int teamID); void TeamChanged(int teamID); + bool ResourceExcess(const std::map & excess); + void PlayerChanged(int playerID); void PlayerAdded(int playerID); void PlayerRemoved(int playerID, int reason); diff --git a/rts/System/Events.def b/rts/System/Events.def index a0e069c27d3..e36f11dac3d 100644 --- a/rts/System/Events.def +++ b/rts/System/Events.def @@ -31,8 +31,12 @@ SETUP_EVENT(GamePaused, MANAGED_BIT) SETUP_EVENT(GameFrame, MANAGED_BIT) SETUP_EVENT(GameFramePost, MANAGED_BIT) + SETUP_EVENT(TeamDied, MANAGED_BIT) SETUP_EVENT(TeamChanged, MANAGED_BIT) + + SETUP_EVENT(ResourceExcess, MANAGED_BIT | CONTROL_BIT) + SETUP_EVENT(PlayerChanged, MANAGED_BIT | UNSYNCED_BIT) SETUP_EVENT(PlayerAdded, MANAGED_BIT | UNSYNCED_BIT) SETUP_EVENT(PlayerRemoved, MANAGED_BIT | UNSYNCED_BIT) From c5fa84d7bf0972c86614b4631b8cc93d09f181e8 Mon Sep 17 00:00:00 2001 From: Keith Harvey Date: Tue, 30 Jun 2026 03:46:48 -0600 Subject: [PATCH 49/90] Add spAddTeamResourceExcessStats Mostly for use with the new ResourceExcess callin. --- rts/Lua/LuaSyncedCtrl.cpp | 47 +++++++++++++++++++++++++++++++++++++++ rts/Lua/LuaSyncedCtrl.h | 1 + 2 files changed, 48 insertions(+) diff --git a/rts/Lua/LuaSyncedCtrl.cpp b/rts/Lua/LuaSyncedCtrl.cpp index d2fc09845cf..6f0a8941d8f 100644 --- a/rts/Lua/LuaSyncedCtrl.cpp +++ b/rts/Lua/LuaSyncedCtrl.cpp @@ -164,6 +164,7 @@ bool LuaSyncedCtrl::PushEntries(lua_State* L) REGISTER_LUA_CFUNC(AddTeamResource); REGISTER_LUA_CFUNC(UseTeamResource); REGISTER_LUA_CFUNC(SetTeamResource); + REGISTER_LUA_CFUNC(AddTeamResourceExcessStats); REGISTER_LUA_CFUNC(SetTeamShareLevel); REGISTER_LUA_CFUNC(ShareTeamResource); @@ -1404,6 +1405,52 @@ int LuaSyncedCtrl::SetTeamShareLevel(lua_State* L) } +/*** + * Records resource excess for a team without moving resources. + * + * The engine normally tracks excess, but if you use `gadget:ResourceExcess` + * to handle it manually it's now also up to you to track stats. + * + * @function Spring.AddTeamResourceExcessStats + * @param teamID integer + * @param type ResourceName + * @param excess number Amount wasted this tick. + * @return nil + */ +int LuaSyncedCtrl::AddTeamResourceExcessStats(lua_State* L) +{ + const int teamID = luaL_checkint(L, 1); + + if (!teamHandler.IsValidTeam(teamID)) + return 0; + + if (!CanControlTeam(L, teamID)) + return 0; + + CTeam* team = teamHandler.Team(teamID); + + if (team == nullptr) + return 0; + + const char rtype = luaL_checkstring(L, 2)[0]; + if (rtype != 'm' && rtype != 'e') + return 0; + + const bool isMetal = (rtype == 'm'); + const float val = std::max(0.0f, luaL_checkfloat(L, 3)); + + float& resExcess = isMetal ? team->resPrevExcess.metal : team->resPrevExcess.energy; + + TeamStatistics& stats = team->GetCurrentStats(); + float& statExcess = isMetal ? stats.metalExcess : stats.energyExcess; + + resExcess += val; + statExcess += val; + + return 0; +} + + /*** Transfers resources between two teams. * Transfers directly, without involving AllowResourceTransfer callin. * Approximately equivalent to doing Use and Add for the sender and receiver, diff --git a/rts/Lua/LuaSyncedCtrl.h b/rts/Lua/LuaSyncedCtrl.h index 84239a079af..3b59dcae8cc 100644 --- a/rts/Lua/LuaSyncedCtrl.h +++ b/rts/Lua/LuaSyncedCtrl.h @@ -52,6 +52,7 @@ class LuaSyncedCtrl static int AddTeamResource(lua_State* L); static int UseTeamResource(lua_State* L); static int SetTeamResource(lua_State* L); + static int AddTeamResourceExcessStats(lua_State* L); static int SetTeamShareLevel(lua_State* L); static int ShareTeamResource(lua_State* L); From d5cc4954bd8dbf8c238b7cacf407142636ea21b4 Mon Sep 17 00:00:00 2001 From: Robert Burnham Date: Wed, 15 Jul 2026 09:02:53 -0500 Subject: [PATCH 50/90] Add debug.emulateMouseWheel input emulation --- rts/Lua/LuaDebugExtra.cpp | 11 +++++++++++ rts/Lua/LuaDebugExtra.h | 1 + 2 files changed, 12 insertions(+) diff --git a/rts/Lua/LuaDebugExtra.cpp b/rts/Lua/LuaDebugExtra.cpp index 7b840cba4c3..1c1a35377d5 100644 --- a/rts/Lua/LuaDebugExtra.cpp +++ b/rts/Lua/LuaDebugExtra.cpp @@ -45,6 +45,7 @@ bool LuaDebugExtra::PushEntries(lua_State* L) LuaPushNamedCFunc(L, "emulateMousePress", EmulateMousePress); LuaPushNamedCFunc(L, "emulateMouseRelease", EmulateMouseRelease); LuaPushNamedCFunc(L, "emulateMouseMove", EmulateMouseMove); + LuaPushNamedCFunc(L, "emulateMouseWheel", EmulateMouseWheel); LuaPushNamedCFunc(L, "clearEmulatedInput", ClearEmulatedInputLua); return true; @@ -138,6 +139,16 @@ int LuaDebugExtra::EmulateMousePress(lua_State* L) return 0; } +int LuaDebugExtra::EmulateMouseWheel(lua_State* L) +{ + if (mouse == nullptr) + return 0; + + // momentary tick, no persistent state to track; fire directly like a real wheel event + mouse->MouseWheel((float)luaL_checknumber(L, 1)); + return 0; +} + /*** Emulate a held mouse button being released. * diff --git a/rts/Lua/LuaDebugExtra.h b/rts/Lua/LuaDebugExtra.h index 35d3c6dba09..1d552c08615 100644 --- a/rts/Lua/LuaDebugExtra.h +++ b/rts/Lua/LuaDebugExtra.h @@ -21,6 +21,7 @@ class LuaDebugExtra { static int EmulateMousePress(lua_State* L); static int EmulateMouseRelease(lua_State* L); static int EmulateMouseMove(lua_State* L); + static int EmulateMouseWheel(lua_State* L); static int ClearEmulatedInputLua(lua_State* L); }; From 75e64c5f2033ca67953c853c79262567f3989d26 Mon Sep 17 00:00:00 2001 From: Robert Burnham Date: Fri, 17 Jul 2026 05:03:12 -0500 Subject: [PATCH 51/90] Add a `cancelcommand` action --- rts/Game/UI/GuiHandler.cpp | 16 +++++++++------- rts/Game/UI/GuiHandler.h | 1 + rts/Game/UnsyncedGameCommands.cpp | 17 +++++++++++++++++ 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/rts/Game/UI/GuiHandler.cpp b/rts/Game/UI/GuiHandler.cpp index 370e21cbe93..6cf530ad46c 100644 --- a/rts/Game/UI/GuiHandler.cpp +++ b/rts/Game/UI/GuiHandler.cpp @@ -1851,16 +1851,18 @@ int CGuiHandler::GetIconPosCommand(int slot) const // only called by SetActiveCo } +void CGuiHandler::CancelActiveCommand() +{ + activeMousePress = false; + SetActiveCommandIndex(-1); +} + + bool CGuiHandler::KeyPressed(int keyCode, int scanCode, bool isRepeat) { RECOIL_DETAILED_TRACY_ZONE; - if (keyCode == SDLK_ESCAPE && activeMousePress) { - activeMousePress = false; - SetActiveCommandIndex(-1); - return true; - } - if (keyCode == SDLK_ESCAPE && inCommand >= 0) { - SetActiveCommandIndex(-1); + if (keyCode == SDLK_ESCAPE && (activeMousePress || inCommand >= 0)) { + CancelActiveCommand(); return true; } diff --git a/rts/Game/UI/GuiHandler.h b/rts/Game/UI/GuiHandler.h index ee22de092ed..de1625aaf99 100644 --- a/rts/Game/UI/GuiHandler.h +++ b/rts/Game/UI/GuiHandler.h @@ -99,6 +99,7 @@ class CGuiHandler : public CInputReceiver { bool SetActiveCommand(int cmdIndex, bool rightMouseButton); bool SetActiveCommand(int cmdIndex, int button, bool leftMouseButton, bool rightMouseButton, bool alt, bool ctrl, bool meta, bool shift); bool SetActiveCommand(const Action& action, const CKeySet& ks, int actionIndex); + void CancelActiveCommand(); void SetDrawSelectionInfo(bool dsi) { drawSelectionInfo = dsi; } bool GetDrawSelectionInfo() const { return drawSelectionInfo; } diff --git a/rts/Game/UnsyncedGameCommands.cpp b/rts/Game/UnsyncedGameCommands.cpp index c49edc0a6a7..19a96edd4b7 100644 --- a/rts/Game/UnsyncedGameCommands.cpp +++ b/rts/Game/UnsyncedGameCommands.cpp @@ -254,6 +254,22 @@ class DeselectActionExecutor : public IUnsyncedActionExecutor { +class CancelCommandActionExecutor : public IUnsyncedActionExecutor { +public: + CancelCommandActionExecutor() : IUnsyncedActionExecutor("CancelCommand", "Cancels the active command (build/order mode)") { + } + + bool Execute(const UnsyncedAction& action) const final { + if (guihandler == nullptr) + return false; + + guihandler->CancelActiveCommand(); + return true; + } +}; + + + class MapMeshDrawerActionExecutor : public IUnsyncedActionExecutor { public: MapMeshDrawerActionExecutor() : IUnsyncedActionExecutor("mapmeshdrawer", "Switch map-mesh rendering modes: 0=GCM, 1=HLOD, 2=ROAM") { @@ -4038,6 +4054,7 @@ void UnsyncedGameCommands::AddDefaultActionExecutors() AddActionExecutor(AllocActionExecutor()); AddActionExecutor(AllocActionExecutor()); AddActionExecutor(AllocActionExecutor()); + AddActionExecutor(AllocActionExecutor()); AddActionExecutor(AllocActionExecutor()); AddActionExecutor(AllocActionExecutor()); AddActionExecutor(AllocActionExecutor()); From 708f0748b78ebc0e3ba45f9b31cceed88c98ac6f Mon Sep 17 00:00:00 2001 From: sprunk Date: Wed, 6 May 2026 15:51:34 +0200 Subject: [PATCH 52/90] Update changelog. The rest of the changelog is not yet up to date. This is an old entry that had waited a bit too long. --- doc/site/content/changelogs/_index.markdown | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/doc/site/content/changelogs/_index.markdown b/doc/site/content/changelogs/_index.markdown index 0e073538cb7..993a030f907 100644 --- a/doc/site/content/changelogs/_index.markdown +++ b/doc/site/content/changelogs/_index.markdown @@ -50,6 +50,20 @@ This is the bleeding-edge changelog since version 2025.06, for **pre-release 202 - unsynced LuaRules (incl. unsynced LuaGaia) now has access to `io` and `os` libraries. - unsynced LuaRules (incl. unsynced LuaGaia) now has access to the `debug` library by default (no longer requires devmode). +### Custom teamcolor palette +- add `Spring.SetCustomPaletteColor(paletteID, r, g, b) → nil`. Sets a palette color for shader use. See below. +- add `Spring.GetCustomPaletteColor(paletteID) → r, g, b`. +- add `Engine.maxCustomPaletteID`, the highest available palette ID. +- add `Spring.SetUnitPaletteIndex(unitID, paletteID?) → nil`. Assigns a paletteID to a unit. Use nil to reset to the default palette. +- add `Spring.GetUnitPaletteIndex(unitID) → paletteID?`. +- add `Spring.SetFeaturePaletteIndex(featureID, paletteID?)`. +- add `Spring.GetFeaturePaletteIndex(featureID) → paletteID?`. +- the `teamColor` array in shaders now has much more room and contains both teamcolors and colors of the custom palette, as per above. +- the actual teamID is now available as the fifth byte (first byte of the second 4-byte composite) in model uniform data. +- note that the value of the palette index is different than what is seen from Lua. Units with no custom paletteID have the index point to an entry that contains their team color. +- the basecontent teamcolor shader takes the above changes into account. Existing custom shaders that use `instData.z & 0xFF` for teamID should keep working as long as the palette feature isn't used, to support it properly the constant needs to be `0x7FF` instead. +- projectiles, and ghosts (buildings out of sight and queued things) unchanged, they still just draw teamcolor naively and cannot use a shader. + ### Replay path getters - add `Spring.GetReplayFilePath() → string?`, returns path of replay being watched. - add `Spring.GetReplayRecordingFilePath() → string?`, returns path of replay to be produced. Note that this is just a prospective file path (nothing is written until the match ends), and that it possible to record a replay of a replay. From 2dd6918b7983678a3308a0312671c5d43bbc6db0 Mon Sep 17 00:00:00 2001 From: bruno-dasilva <8520801+bruno-dasilva@users.noreply.github.com> Date: Sat, 18 Jul 2026 17:20:59 -0700 Subject: [PATCH 53/90] feat: Add GetPrevFrameChecksum() to the Lua API (#2922) Expose the sim frame checksum information to lua so we can update the synctest widget/gadget combo to grab it every sim frame and dump it to a file at the end of each synctest run. --- rts/Lua/LuaConstEngine.cpp | 1 - rts/Lua/LuaConstPlatform.cpp | 7 +++++++ rts/Lua/LuaUnsyncedRead.cpp | 31 +++++++++++++++++++++++++++++++ rts/Lua/LuaUnsyncedRead.h | 1 + rts/Net/NetCommands.cpp | 6 ++++++ rts/System/Sync/SyncChecker.cpp | 1 + rts/System/Sync/SyncChecker.h | 7 +++++++ 7 files changed, 53 insertions(+), 1 deletion(-) diff --git a/rts/Lua/LuaConstEngine.cpp b/rts/Lua/LuaConstEngine.cpp index 0d38c1ffbf0..579a07102f9 100644 --- a/rts/Lua/LuaConstEngine.cpp +++ b/rts/Lua/LuaConstEngine.cpp @@ -45,7 +45,6 @@ * @field wordSize number Indicates the build type always 64 these days * @field gameSpeed number Number of simulation gameframes per second * @field textColorCodes TextColorCode Table containing keys that represent the color code operations during font rendering - * @field isHeadless boolean? Whether this is a headless engine build. Not available in synced */ bool LuaConstEngine::PushEntries(lua_State* L) diff --git a/rts/Lua/LuaConstPlatform.cpp b/rts/Lua/LuaConstPlatform.cpp index 7262bd7d5a5..9c23cd29db5 100644 --- a/rts/Lua/LuaConstPlatform.cpp +++ b/rts/Lua/LuaConstPlatform.cpp @@ -151,5 +151,12 @@ bool LuaConstPlatform::PushEntries(lua_State* L) /*** @field Platform.isHeadless boolean Is this a headless build which only simulates and doesnt offer interactive IO? */ LuaPushNamedBool(L, "isHeadless", SpringVersion::IsHeadless()); + /*** @field Platform.hasSyncChecksums boolean Whether the engine was built with sync-check support (i.e. Spring.GetPrevFrameSyncChecksum() returns a meaningful value). */ + #ifdef SYNCCHECK + LuaPushNamedBool(L, "hasSyncChecksums", true); + #else + LuaPushNamedBool(L, "hasSyncChecksums", false); + #endif + return true; } diff --git a/rts/Lua/LuaUnsyncedRead.cpp b/rts/Lua/LuaUnsyncedRead.cpp index f1615b8079b..8b4e95a9e0b 100644 --- a/rts/Lua/LuaUnsyncedRead.cpp +++ b/rts/Lua/LuaUnsyncedRead.cpp @@ -75,6 +75,7 @@ #include "System/Sound/ISound.h" #include "System/Sound/ISoundChannels.h" #include "System/StringUtil.h" +#include "System/Sync/SyncChecker.h" #include "System/Misc/SpringTime.h" #include "System/ScopedResource.h" #include "System/Math/NURBS.h" @@ -120,6 +121,7 @@ bool LuaUnsyncedRead::PushEntries(lua_State* L) REGISTER_LUA_CFUNC(GetGameSecondsInterpolated); REGISTER_LUA_CFUNC(GetLastUpdateSeconds); REGISTER_LUA_CFUNC(GetVideoCapturingMode); + REGISTER_LUA_CFUNC(GetPrevFrameSyncChecksum); REGISTER_LUA_CFUNC(GetNumDisplays); REGISTER_LUA_CFUNC(GetViewGeometry); @@ -1236,6 +1238,35 @@ int LuaUnsyncedRead::GetVideoCapturingMode(lua_State* L) } +/*** + * + * Returns the engine's sync checksum for the previous simframe, + * useful for testing. The returned string is NOT convertible to + * a number within Lua. + * + * Returns a dummy value if `Platform.hasSyncChecksums` is false, + * or if no frames were processed yet. + * + * @function Spring.GetPrevFrameSyncChecksum + * + * @return string checksum + */ +int LuaUnsyncedRead::GetPrevFrameSyncChecksum(lua_State* L) +{ +#ifdef SYNCCHECK + unsigned checksum = CSyncChecker::GetPrevChecksum(); +#else + unsigned checksum = 0; +#endif + + char buf[9]; + snprintf(buf, sizeof(buf), "%08x", checksum); + lua_pushstring(L, buf); + + return 1; +} + + /****************************************************************************** * Unit attributes * @section unitattributes diff --git a/rts/Lua/LuaUnsyncedRead.h b/rts/Lua/LuaUnsyncedRead.h index 2d26b5544fa..3926912fdca 100644 --- a/rts/Lua/LuaUnsyncedRead.h +++ b/rts/Lua/LuaUnsyncedRead.h @@ -32,6 +32,7 @@ class LuaUnsyncedRead { static int GetGameSecondsInterpolated(lua_State* L); static int GetLastUpdateSeconds(lua_State* L); static int GetVideoCapturingMode(lua_State* L); + static int GetPrevFrameSyncChecksum(lua_State* L); static int GetNumDisplays(lua_State* L); static int GetViewGeometry(lua_State* L); diff --git a/rts/Net/NetCommands.cpp b/rts/Net/NetCommands.cpp index 124d44b8175..55d5d0071d8 100644 --- a/rts/Net/NetCommands.cpp +++ b/rts/Net/NetCommands.cpp @@ -623,6 +623,12 @@ void CGame::ClientReadNet() ASSERT_SYNCED(CSyncChecker::GetChecksum()); clientNet->Send(CBaseNetProtocol::Get().SendSyncResponse(gu->myPlayerNum, gs->frameNum, CSyncChecker::GetChecksum())); + // Cache the just-closed frame's checksum so Lua can read a + // stable value during the next sim frame via + // Spring.GetPrevFrameSyncChecksum(). Must run before the + // 4096-frame reset below so we capture the pre-reset value. + CSyncChecker::SetPrevChecksum(CSyncChecker::GetChecksum()); + // buffer all checksums, so we can check sync later between demo & local if (haveServerDemo) localSyncChecksums[gs->frameNum] = CSyncChecker::GetChecksum(); diff --git a/rts/System/Sync/SyncChecker.cpp b/rts/System/Sync/SyncChecker.cpp index 4063faff100..edb0cb02e4b 100644 --- a/rts/System/Sync/SyncChecker.cpp +++ b/rts/System/Sync/SyncChecker.cpp @@ -11,6 +11,7 @@ unsigned CSyncChecker::g_checksum; +unsigned CSyncChecker::g_prevChecksum; int CSyncChecker::inSyncedCode; void CSyncChecker::NewFrame() diff --git a/rts/System/Sync/SyncChecker.h b/rts/System/Sync/SyncChecker.h index 35aa1be6d7a..3fb996c262a 100644 --- a/rts/System/Sync/SyncChecker.h +++ b/rts/System/Sync/SyncChecker.h @@ -31,6 +31,8 @@ class CSyncChecker { * Keeps a running checksum over all assignments to synced variables. */ static unsigned GetChecksum() { return g_checksum; } + static unsigned GetPrevChecksum() { return g_prevChecksum; } + static void SetPrevChecksum(unsigned v) { g_prevChecksum = v; } static void NewFrame(); static void debugSyncCheckThreading(); static void Sync(uint32_t val); @@ -48,6 +50,11 @@ class CSyncChecker { */ static unsigned g_checksum; + /** + * Final checksum of the previous simulation frame. + */ + static unsigned g_prevChecksum; + /** * @brief in synced code * From e457c0042f5c897d02bead64fe852e1a6d09317e Mon Sep 17 00:00:00 2001 From: bruno-dasilva <8520801+bruno-dasilva@users.noreply.github.com> Date: Sat, 18 Jul 2026 17:10:40 -0700 Subject: [PATCH 54/90] feat: add multi-platform sync testing (amd64-windows/linux + arm64-linux) (#2921) --- .github/workflows/synctest.yml | 273 +++++++++++++++++++++++++ test/synctest/README.md | 42 ++++ test/synctest/synctest-startscript.txt | 57 ++++++ 3 files changed, 372 insertions(+) create mode 100644 .github/workflows/synctest.yml create mode 100644 test/synctest/README.md create mode 100644 test/synctest/synctest-startscript.txt diff --git a/.github/workflows/synctest.yml b/.github/workflows/synctest.yml new file mode 100644 index 00000000000..89fa59a471a --- /dev/null +++ b/.github/workflows/synctest.yml @@ -0,0 +1,273 @@ +# Runs the Beyond All Reason "synctest" (/luarules synctest) against an engine +# commit on all supported platforms (amd64-linux, arm64-linux, amd64-windows). +# +# This workflow looks up the most recent successful "Build Engine v2" run for +# the target commit and downloads platform-specific artifacts. If no such run +# exists, the workflow fails fast. +# TODO: trigger after every engine build +# +# The cross-platform-check job compares hashes across architectures and is +# intended as a merge-blocking required check. +# +# See test/synctest/README.md for background and how to run locally. +name: Sync test + +on: + workflow_dispatch: + inputs: + commit: + description: 'Engine commit SHA to test. Leave blank to test the head of the ref selected in the picker above.' + type: string + default: '' + +permissions: + # actions:read is required to list workflow runs and download artifacts + # produced by a different workflow run (the Build Engine v2 run for the + # target commit). + actions: read + contents: read + +defaults: + run: + shell: bash -euo pipefail {0} + +env: + MAP: "Jade Empress 1.41" + GAME: "Beyond All Reason test-30212-63c6ded" + HASH_FILE: "synctest_synchash.json" + +jobs: + setup: + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + sha: ${{ steps.resolve.outputs.sha }} + run_id: ${{ steps.engine-run.outputs.run_id }} + platforms: ${{ steps.platforms.outputs.json }} + env: + GH_TOKEN: ${{ github.token }} + steps: + - name: Define target platforms + id: platforms + # Single source of truth for the platform matrix: needed so we can check + # if we have all platforms even if a later setup step fails. + run: | + # jq -c -n validates the JSON and emits it compact, as GITHUB_OUTPUT requires. + json=$(jq -c -n '[ + {"platform": "amd64-linux", "runs-on": "ubuntu-latest", "bin-suffix": ""}, + {"platform": "arm64-linux", "runs-on": "ubuntu-24.04-arm", "bin-suffix": ""}, + {"platform": "amd64-windows", "runs-on": "windows-latest", "bin-suffix": ".exe"} + ]') + echo "json=$json" >> "$GITHUB_OUTPUT" + + - name: Resolve target commit + id: resolve + env: + INPUT_COMMIT: ${{ inputs.commit }} + REPO: ${{ github.repository }} + DEFAULT_SHA: ${{ github.sha }} + run: | + target="${INPUT_COMMIT:-$DEFAULT_SHA}" + + # Normalize whatever the user pasted (full sha, short sha, branch, tag) + # into a full 40-char SHA so the run lookup below is unambiguous. + full_sha=$(gh api "repos/$REPO/commits/$target" --jq '.sha') + echo "Resolved target commit: $full_sha" + echo "sha=$full_sha" >> "$GITHUB_OUTPUT" + + - name: Find Build Engine v2 run for this commit + id: engine-run + env: + REPO: ${{ github.repository }} + TARGET_SHA: ${{ steps.resolve.outputs.sha }} + run: | + run_id=$(gh api \ + "repos/$REPO/actions/workflows/engine-build.yml/runs?head_sha=$TARGET_SHA&status=success" \ + --jq '.workflow_runs[0].id') + if [ -z "$run_id" ] || [ "$run_id" = "null" ]; then + echo "::error::No successful 'Build Engine v2' run found for commit $TARGET_SHA." + echo "::error::Trigger Build Engine v2 on that commit first (push it, or dispatch engine-build.yml manually) and retry." + exit 1 + fi + echo "Using engine-build run $run_id" + echo "run_id=$run_id" >> "$GITHUB_OUTPUT" + + synctest: + needs: setup + runs-on: ${{ matrix.runs-on }} + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + include: ${{ fromJSON(needs.setup.outputs.platforms) }} + steps: + - name: Checkout workflow definition and test inputs + # Uses the ref picked in the dispatch UI (github.sha), NOT the target + # commit — so the startscript and any other test assets come from the + # workflow-definition ref. The target commit only controls which engine + # artifact gets downloaded below. + uses: actions/checkout@v4 + + - name: Download engine artifact (${{ matrix.platform }}) + uses: actions/download-artifact@v4 + with: + pattern: engine-artifacts-${{ matrix.platform }}* + run-id: ${{ needs.setup.outputs.run_id }} + github-token: ${{ github.token }} + path: engine-artifact + merge-multiple: true + + - name: Extract install tree + run: | + mkdir -p install + + # Engine is packaged with INSTALL_PORTABLE=ON (CMake default, see + # CMakeLists.txt:22), so binaries live at the install-tree root: + # install/spring-headless, install/pr-downloader, install/spring, ... + # The archive name comes from docker-build-v2/scripts/package.sh; that + # script also emits a -dbgsym.tar.zst, which this glob excludes. + # + # Windows has 7z.exe, and ubuntu currently has 7z/7zz/7za but may later + # drop to just 7zz/7za. + sevenzip=$(command -v 7z || command -v 7zz || command -v 7za || true) + if [ -z "$sevenzip" ]; then + echo "::error::no 7-Zip binary (7z/7zz/7za) found on runner"; exit 1 + fi + "$sevenzip" x engine-artifact/recoil_*_${{ matrix.platform }}*.7z -oinstall + + - name: Restore BAR content cache + uses: actions/cache@v4 + with: + # Content-addressed and immutable, so all three platform legs share one + # entry. packages/ (the .sdp manifests) is required alongside pool/ + # for the engine to mount the game. Deliberately NOT the whole write-dir: + # everything else the engine writes here is build- and platform-specific. + # rapid/ is excluded too — mutable repo metadata, a few MB, refetched so + # the pin is genuinely re-resolved each run. + path: | + bar-data/pool + bar-data/packages + bar-data/maps + # IMPORTANT: + # Bump this key in lockstep with any change to the pinned + # gametype/mapname. See test/synctest/README.md. + key: bar-data-${{ env.GAME }}-${{ env.MAP }} + + - name: Fetch pinned BAR assets (no-op on warm cache) + run: | + ./install/pr-downloader${{ matrix.bin-suffix }} \ + --filesystem-writepath "$PWD/bar-data" \ + --download-game "$GAME" \ + --download-map "$MAP" + env: + PRD_RAPID_USE_STREAMER: "false" + PRD_RAPID_REPO_MASTER: "https://repos-cdn.beyondallreason.dev/repos.gz" + PRD_HTTP_SEARCH_URL: "https://files-cdn.beyondallreason.dev/find" + + - name: Render startscript + run: | + # The checked-in startscript is a template: @VERSION@ is the version + # suffix of the pinned GAME (text after the last space, e.g. + # "test-29932-21b3bb0") and @MAPNAME@ is MAP. + sed -e "s/@VERSION@/${GAME##* }/g" -e "s/@MAPNAME@/$MAP/g" \ + test/synctest/synctest-startscript.txt > startscript.txt + + # Fail loudly here rather than letting the engine fail later with an + # opaque "can't find game/map" error. + if grep -qE '@VERSION@|@MAPNAME@' startscript.txt; then + echo "::error::startscript template was not fully rendered — leftover placeholders:" + grep -nE '@VERSION@|@MAPNAME@' startscript.txt + exit 1 + fi + grep -nE 'gametype=|mapname=' startscript.txt + + - name: Run synctest + run: | + ./install/spring-headless${{ matrix.bin-suffix }} --isolation \ + --write-dir "$PWD/bar-data" startscript.txt 2>&1 | tee infolog.txt + + - name: Assert sync-hash file produced + run: | + digest=$(jq -r '.digest // empty' "bar-data/$HASH_FILE" 2>/dev/null || true) + if [ -z "$digest" ]; then + echo "::error::bar-data/$HASH_FILE is missing, empty, or has no .digest — the synctest produced no checksum output" + exit 1 + fi + echo "Sync hash digest: $digest" + + - name: Upload run artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: synctest-${{ matrix.platform }}-${{ needs.setup.outputs.sha }} + path: | + infolog.txt + bar-data/*.log + bar-data/${{ env.HASH_FILE }} + retention-days: 30 + if-no-files-found: ignore + + # Collects sync hashes from all synctest runs across platforms and fails if + # hashes diverge between architectures (cross-platform desync detection). + # This job is intended to be a required/merge-blocking status check. + cross-platform-check: + if: ${{ !cancelled() }} + needs: [setup, synctest] + runs-on: ubuntu-latest + timeout-minutes: 5 + env: + TESTED_SHA: ${{ needs.setup.outputs.sha }} + steps: + - name: Assert setup produced a platform list + env: + PLATFORMS_JSON: ${{ needs.setup.outputs.platforms }} + run: | + if [ -z "$PLATFORMS_JSON" ]; then + echo "::error::setup produced no platform list — it likely failed before it ran; check the setup job." + exit 1 + fi + + - name: Download current run artifacts + # If every platform leg died before uploading, the pattern matches + # nothing and this step errors. That is not the failure we want to + # report, so swallow it and let the MISSING check below speak instead. + continue-on-error: true + uses: actions/download-artifact@v4 + with: + pattern: synctest-*-${{ env.TESTED_SHA }} + path: artifacts + + - name: Compare sync hashes across platforms + env: + PLATFORMS_JSON: ${{ needs.setup.outputs.platforms }} + run: | + # Check from the EXPECTED platform list, so if artifact uploading fails + # we will be able to detect that its missing. + for platform in $(jq -r '.[].platform' <<<"$PLATFORMS_JSON"); do + digest=$(jq -r '.digest // empty' \ + "artifacts/synctest-$platform-$TESTED_SHA/bar-data/$HASH_FILE" 2>/dev/null || true) + echo "$platform ${digest:-MISSING}" + done > synchashes.txt + + echo "Cross-platform determinism check:" + cat synchashes.txt + + if grep -qw MISSING synchashes.txt; then + echo "::error::sync hash MISSING on at least one platform — see the per-platform jobs" + exit 1 + fi + digests=$(awk '{print $2}' synchashes.txt | sort -u) + if [ "$(wc -l <<<"$digests")" -gt 1 ]; then + echo "::error::CROSS-PLATFORM DESYNC — hashes differ across architectures; the engine is not deterministic across platforms" + exit 1 + fi + echo "OK across all platforms ($digests)" + + # A platform can write the hash and then fail, but + # `!cancelled()` means we still run the comparison. + # So let's do a final check to make sure they all passed. + - name: Assert all synctest jobs succeeded + if: ${{ needs.synctest.result != 'success' }} + run: | + echo "::error::synctest matrix result is '${{ needs.synctest.result }}' — a platform leg did not succeed; see the per-platform jobs" + exit 1 diff --git a/test/synctest/README.md b/test/synctest/README.md new file mode 100644 index 00000000000..30af8a81fef --- /dev/null +++ b/test/synctest/README.md @@ -0,0 +1,42 @@ +# Synctest + +Engine CI sync test driven by BAR's synctest gadget (see beyond-all-reason/Beyond-All-Reason#7440 and #7445). The `.github/workflows/synctest.yml` workflow runs a single seeded scenario on each supported platform (amd64-linux, arm64-linux, amd64-windows) on top of a pinned BAR game + map and asserts that a deterministic sync-hash JSON file lands in the engine's write-dir. The `cross-platform-check` job then verifies all platforms produced identical digests, catching architecture-specific desyncs. Background: beyond-all-reason/RecoilEngine#2910, beyond-all-reason/RecoilEngine#2906. + +The gadget itself rotates through unit categories (bots, tanks, fighters, bombers, hover, subs, ships, spiders) internally, so one scenario is enough — no per-category scripts needed. + +## Running locally + +Build the engine, make sure `spring-headless` and `pr-downloader` are on your PATH or referenced by absolute path, then — using the same `GAME`/`MAP` values as the `env:` block in `.github/workflows/synctest.yml`: + +``` +GAME="Beyond All Reason test-30212-63c6ded" +MAP="Jade Empress 1.41" + +# The PRD_* vars point at BAR's CDN; without them pr-downloader resolves a +# different rapid master than CI does. +PRD_RAPID_USE_STREAMER=false \ +PRD_RAPID_REPO_MASTER=https://repos-cdn.beyondallreason.dev/repos.gz \ +PRD_HTTP_SEARCH_URL=https://files-cdn.beyondallreason.dev/find \ +pr-downloader --filesystem-writepath ./bar-data \ + --download-game "$GAME" \ + --download-map "$MAP" + +# synctest-startscript.txt is a template — render the pins into it first. +sed -e "s/@VERSION@/${GAME##* }/g" -e "s/@MAPNAME@/$MAP/g" \ + test/synctest/synctest-startscript.txt > startscript.txt + +spring-headless --isolation --write-dir ./bar-data startscript.txt +``` + +A successful run exits 0 at game frame ~2100 (the startscript's trailing `quitforce`) and leaves `bar-data/synctest_synchash.json` — a JSON file whose `digest` field is what CI compares. + +## Bumping the pinned BAR version + +BAR's rapid `test--` tags are content-addressed — once published they never change — which is why the CI doesn't need an external version manifest. The only place to bump is the `GAME:` env in `.github/workflows/synctest.yml` (and `MAP:` for the map): the workflow feeds them to `pr-downloader`, substitutes them into the startscript template's `gametype=Beyond All Reason @VERSION@;` and `mapname=@MAPNAME@;` lines, and uses both in the cache key. + + +## Replacing the startscript + +The startscript was generated by BAR from running a fightertest scenario in the actual BAR lobby, then pulled from the bar data dir; modifying it to instead run synctest on a different map. If it ever needs changing, that's the easiest way to regenerate it. + +A trailing `quitforce` in the startscript's `debugcommands` is required so the engine actually quits when the test finishes. diff --git a/test/synctest/synctest-startscript.txt b/test/synctest/synctest-startscript.txt new file mode 100644 index 00000000000..ce644af5d6e --- /dev/null +++ b/test/synctest/synctest-startscript.txt @@ -0,0 +1,57 @@ +[game] +{ + [allyteam1] + { + startrectleft=0.82999998; + startrectright=1; + startrectbottom=1; + numallies=0; + startrecttop=0; + } + [team0] + { + startposx=800; + allyteam=0; + teamleader=0; + side=Armada; + startposz=100; + } + [allyteam0] + { + startrectleft=0; + startrectright=0.17; + startrectbottom=1; + numallies=0; + startrecttop=0; + } + [modoptions] + { + debugcommands=2:cheat|3:globallos|15:luarules synctest 2000 0.33 0.33 0.1 0.5|20:setspeed 20|21:speedcontrol 0|2100:quitforce; + } + [player0] + { + team=0; + name=Player; + } + [ai0] + { + team=1; + host=0; + shortname=NullAI; + } + [team1] + { + allyteam=1; + teamleader=0; + side=Cortex; + startposx=100; + startposz=800; + } + ishost=1; + startpostype=3; + gametype=Beyond All Reason @VERSION@; + mapname=@MAPNAME@; + myplayername=Player; + fixedrngseed=123123; + numplayers=1; +} From 8059a16670d1a5f56da77d10808c183dc351119f Mon Sep 17 00:00:00 2001 From: bruno-dasilva <8520801+bruno-dasilva@users.noreply.github.com> Date: Mon, 20 Jul 2026 02:37:43 -0700 Subject: [PATCH 55/90] Bump game version in synctest.yml Engine moved the sync feature flag. --- .github/workflows/synctest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/synctest.yml b/.github/workflows/synctest.yml index 89fa59a471a..c01baeaa60a 100644 --- a/.github/workflows/synctest.yml +++ b/.github/workflows/synctest.yml @@ -33,7 +33,7 @@ defaults: env: MAP: "Jade Empress 1.41" - GAME: "Beyond All Reason test-30212-63c6ded" + GAME: "Beyond All Reason test-30707-555e3a1" HASH_FILE: "synctest_synchash.json" jobs: From 359436da6f0d9bb2e45abe4ac71cfb4b30a92dcf Mon Sep 17 00:00:00 2001 From: Robert Burnham Date: Mon, 20 Jul 2026 14:39:09 -0500 Subject: [PATCH 56/90] Fix `/unbind` for keychains `/unbind` couldn't handle chained keysets: bind accepts "a,b" but unbind parsed the key string as a single keyset, so unbinding a keychain always failed with "could not parse key". This parses the key string with the same keychain parser bind uses and looks the binding up under the chain's last keyset, which is where bind stores it. Removal matching is unchanged. --- rts/Game/UI/KeyBindings.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/rts/Game/UI/KeyBindings.cpp b/rts/Game/UI/KeyBindings.cpp index 4dd9e097110..bde8e3efb16 100644 --- a/rts/Game/UI/KeyBindings.cpp +++ b/rts/Game/UI/KeyBindings.cpp @@ -662,8 +662,8 @@ bool CKeyBindings::Bind(const std::string& keystr, const std::string& line) bool CKeyBindings::UnBind(const std::string& keystr, const std::string& command) { RECOIL_DETAILED_TRACY_ZONE; - CKeySet ks; - if (!ks.Parse(keystr)) { + CKeyChain kc; + if (!ParseKeyChain(keystr, &kc) || kc.empty()) { LOG_L(L_WARNING, "UnBind: could not parse key: %s", keystr.c_str()); return false; } @@ -671,6 +671,7 @@ bool CKeyBindings::UnBind(const std::string& keystr, const std::string& command) if (debugEnabled) LOG("[CKeyBindings::%s] keystr=%s command=%s", __func__, keystr.c_str(), command.c_str()); + const CKeySet& ks = kc.back(); KeyMap& bindings = ks.IsKeyCode() ? codeBindings : scanBindings; const auto it = bindings.find(ks); From a23be68557b23df3772388dafc934a3bac6fe8b8 Mon Sep 17 00:00:00 2001 From: Robert Burnham Date: Tue, 21 Jul 2026 08:26:14 -0500 Subject: [PATCH 57/90] Fix stale `spGetActionHotKeys` returns (#3082) Rebuild the hotkey map on actual binding changes. --- rts/Game/UI/KeyBindings.cpp | 69 +++++++++++++++++++++++++------------ rts/Game/UI/KeyBindings.h | 10 +++++- 2 files changed, 56 insertions(+), 23 deletions(-) diff --git a/rts/Game/UI/KeyBindings.cpp b/rts/Game/UI/KeyBindings.cpp index bde8e3efb16..cc756cd26d5 100644 --- a/rts/Game/UI/KeyBindings.cpp +++ b/rts/Game/UI/KeyBindings.cpp @@ -611,6 +611,7 @@ void CKeyBindings::AddActionToKeyMap(KeyMap& bindings, Action& action) ActionList& al = bindings[ks]; action.bindingIndex = ++bindingsCount; al.push_back(action); + buildHotkeyMap = true; } else { ActionList& al = it->second; assert(it->first == ks); @@ -624,6 +625,7 @@ void CKeyBindings::AddActionToKeyMap(KeyMap& bindings, Action& action) // not yet bound, push it action.bindingIndex = ++bindingsCount; al.push_back(action); + buildHotkeyMap = true; } } } @@ -684,6 +686,9 @@ bool CKeyBindings::UnBind(const std::string& keystr, const std::string& command) if (al.empty()) bindings.erase(it); + if (success) + buildHotkeyMap = true; + return success; } @@ -708,6 +713,7 @@ bool CKeyBindings::UnBindKeyset(const std::string& keystr) return false; bindings.erase(it); + buildHotkeyMap = true; return true; } @@ -741,13 +747,16 @@ bool CKeyBindings::UnBindAction(const std::string& command) RECOIL_DETAILED_TRACY_ZONE; if (debugEnabled) LOG("[CKeyBindings::%s] command=%s", __func__, command.c_str()); - // clear both maps; || would short-circuit and leave the scancode binding when // the action is also bound to a keycode const bool removedFromCode = RemoveActionFromKeyMap(command, codeBindings); const bool removedFromScan = RemoveActionFromKeyMap(command, scanBindings); + const bool changed = removedFromCode || removedFromScan; + + if (changed) + buildHotkeyMap = true; - return removedFromCode || removedFromScan; + return changed; } @@ -818,9 +827,6 @@ void CKeyBindings::ConfigNotify(const std::string& key, const std::string& value void CKeyBindings::LoadDefaults() { RECOIL_DETAILED_TRACY_ZONE; - const bool tmpBuildHotkeyMap = buildHotkeyMap; - buildHotkeyMap = false; - if (debugEnabled) LOG("[CKeyBindings::%s]", __func__); @@ -829,8 +835,7 @@ void CKeyBindings::LoadDefaults() for (const auto& b: defaultBindings) { Bind(b.key, b.action); } - - buildHotkeyMap = tmpBuildHotkeyMap; + // no rebuild here: only ever used as a building block, the caller rebuilds } @@ -865,7 +870,7 @@ void CKeyBindings::PushAction(const Action& action) } } -bool CKeyBindings::ExecuteCommand(const std::string& line) +bool CKeyBindings::ExecuteCommandInternal(const std::string& line) { RECOIL_DETAILED_TRACY_ZONE; const std::vector words = CSimpleParser::Tokenize(line, 2); @@ -894,7 +899,7 @@ bool CKeyBindings::ExecuteCommand(const std::string& line) if (loadStack.empty() && words.size() == 1) LoadDefaults(); - Load(filename); + LoadInternal(filename); } else if (command == "keyreload") { const std::string& filename = words.size() > 1 ? words[1] : DEFAULT_FILENAME; @@ -902,13 +907,13 @@ bool CKeyBindings::ExecuteCommand(const std::string& line) if (debugEnabled) LOG("[CKeyBindings::%s] line=%s", __func__, line.c_str()); - ExecuteCommand("unbindall"); - ExecuteCommand("unbind enter chat"); + ExecuteCommandInternal("unbindall"); + ExecuteCommandInternal("unbind enter chat"); if (loadStack.empty() && words.size() == 1) LoadDefaults(); - Load(filename); + LoadInternal(filename); } else if (command == "keydefaults") { LoadDefaults(); @@ -937,6 +942,7 @@ bool CKeyBindings::ExecuteCommand(const std::string& line) keyCodes.Reset(); scanCodes.Reset(); bindingsCount = 0; + buildHotkeyMap = true; Bind("enter", "chat"); // bare minimum if (debugEnabled) @@ -946,14 +952,19 @@ bool CKeyBindings::ExecuteCommand(const std::string& line) return false; } - if (buildHotkeyMap) - BuildHotkeyMap(); - return false; } -bool CKeyBindings::Load(const std::string& filename) +bool CKeyBindings::ExecuteCommand(const std::string& line) +{ + const bool ret = ExecuteCommandInternal(line); + MaybeBuildHotkeyMap(); + return ret; +} + + +bool CKeyBindings::LoadInternal(const std::string& filename) { RECOIL_DETAILED_TRACY_ZONE; if (std::find(loadStack.begin(), loadStack.end(), filename) != loadStack.end()) { @@ -965,9 +976,6 @@ bool CKeyBindings::Load(const std::string& filename) return false; } - const bool tmpBuildHotkeyMap = buildHotkeyMap; - buildHotkeyMap = false; - if (debugEnabled) { LOG("[CKeyBindings::%s] filename=%s%s", __func__, filename.c_str(), loadStack.empty() ? "" : ", load stack:"); for (auto it = loadStack.rbegin(); it != loadStack.rend(); ++it) @@ -980,17 +988,34 @@ bool CKeyBindings::Load(const std::string& filename) CSimpleParser parser(ifs); while (!parser.Eof()) { - ExecuteCommand(parser.GetCleanLine()); + ExecuteCommandInternal(parser.GetCleanLine()); } loadStack.pop_back(); - buildHotkeyMap = tmpBuildHotkeyMap; - return true; } +bool CKeyBindings::Load(const std::string& filename) +{ + const bool ret = LoadInternal(filename); + MaybeBuildHotkeyMap(); + return ret; +} + + +void CKeyBindings::MaybeBuildHotkeyMap() +{ + RECOIL_DETAILED_TRACY_ZONE; + if (!buildHotkeyMap) + return; + + BuildHotkeyMap(); + buildHotkeyMap = false; +} + + void CKeyBindings::BuildHotkeyMap() { RECOIL_DETAILED_TRACY_ZONE; diff --git a/rts/Game/UI/KeyBindings.h b/rts/Game/UI/KeyBindings.h index 04260a7993e..9caf37e6102 100644 --- a/rts/Game/UI/KeyBindings.h +++ b/rts/Game/UI/KeyBindings.h @@ -58,8 +58,16 @@ class CKeyBindings : public CommandReceiver protected: void BuildHotkeyMap(); + // rebuilds the reverse map once, and only if a binding actually changed + void MaybeBuildHotkeyMap(); void DebugActionList(const ActionList& actionList) const; + // the *Internal variants do the work but never rebuild the reverse map; + // the public Load/ExecuteCommand wrappers rebuild once at the end, so every + // outside call path rebuilds exactly once regardless of how it recurses + bool LoadInternal(const std::string& filename = DEFAULT_FILENAME); + bool ExecuteCommandInternal(const std::string& line); + void AddActionToKeyMap(KeyMap& bindings, Action& action); static bool RemoveActionFromKeyMap(const std::string& command, KeyMap& bindings); @@ -90,7 +98,7 @@ class CKeyBindings : public CommandReceiver int fakeMetaKey = -1; int keyChainTimeout = 750; - bool buildHotkeyMap = true; + bool buildHotkeyMap = true; // reverse hotkey map is stale and needs rebuilding bool debugEnabled = false; }; From 48e9b0073e5163a2df6c44c2b8a484fc05a52661 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Ptaszek?= Date: Wed, 22 Jul 2026 12:26:28 +0200 Subject: [PATCH 58/90] Fixes to `Spring.SetMapShader` (#3127) * fix forward-only map shaders never activating * fix stale uniforms remaining after shader swap * add `Engine.FeatureSupport.reliableLuaMapShaders` feature flag --- rts/Lua/LuaConstEngine.cpp | 2 ++ rts/Map/SMF/SMFRenderState.cpp | 6 ++++++ rts/Rendering/Shaders/Shader.h | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/rts/Lua/LuaConstEngine.cpp b/rts/Lua/LuaConstEngine.cpp index 579a07102f9..ce9defc5e50 100644 --- a/rts/Lua/LuaConstEngine.cpp +++ b/rts/Lua/LuaConstEngine.cpp @@ -28,6 +28,7 @@ * @field noHandicapForReclaim boolean Whether handicap is applied to income from reclaim * @field groupAddDoesntSelect boolean Whether 'group add' also selects the group (does both if false) * @field deadTeamsKeepUnitLimit boolean Whether engine redistributes dead team unitlimit to allies (false) or keeps it as-is (true) + * @field reliableLuaMapShaders boolean Whether forward-only Lua map shaders activate without a deferred draw and Spring.SetMapShader program swaps refresh cached uniform locations */ /*** @@ -83,6 +84,7 @@ bool LuaConstEngine::PushEntries(lua_State* L) LuaPushNamedBool(L, "noHandicapForReclaim", true); LuaPushNamedBool(L, "groupAddDoesntSelect", true); LuaPushNamedBool(L, "deadTeamsKeepUnitLimit", false); + LuaPushNamedBool(L, "reliableLuaMapShaders", true); lua_rawset(L, -3); lua_pushliteral(L, "textColorCodes"); diff --git a/rts/Map/SMF/SMFRenderState.cpp b/rts/Map/SMF/SMFRenderState.cpp index b116563bd87..67ec03ff6e4 100644 --- a/rts/Map/SMF/SMFRenderState.cpp +++ b/rts/Map/SMF/SMFRenderState.cpp @@ -94,6 +94,12 @@ void SMFRenderStateGLSL::Update( for (uint32_t n = GLSL_SHADER_FWD_ADV; n <= GLSL_SHADER_DFR_ADV; n++) { glslShaders[n]->LoadFromID(luaMapShaderData->shaderIDs[n - 1]); } + + // currShader is null from Init() (GLSL_SHADER_FWD_STD is never created for + // the Lua state), so re-evaluate it now that program IDs changed; otherwise + // HasValidShader(Normal) stays false and a forward-only Lua map shader is + // never selected until a deferred Lua draw happens to run first + SetCurrentShader(smfGroundDrawer, DrawPass::Normal); } else { assert(luaMapShaderData == nullptr); diff --git a/rts/Rendering/Shaders/Shader.h b/rts/Rendering/Shaders/Shader.h index 90fa492d76d..8f0945ce655 100644 --- a/rts/Rendering/Shaders/Shader.h +++ b/rts/Rendering/Shaders/Shader.h @@ -131,6 +131,10 @@ namespace Shader { // not needed for pre-compiled programs shaderObjs.clear(); + // cached locations/values belong to the previous program; without this, + // uniforms set by name (e.g. texSquare) target stale locations after a + // Spring.SetMapShader program swap + uniformStates.clear(); } /// create the whole shader from a lua file From 8e677dc0d77e6589bc0b02dfafbd663d6b4b91a9 Mon Sep 17 00:00:00 2001 From: tristan Date: Thu, 23 Jul 2026 00:34:56 +1000 Subject: [PATCH 59/90] Optimize LuaPushNamedFoo using compile time key hashing (#2986) Add string literal overloads for LuaPushNamedFoo that enable a compile time hash. Remove legacy hashing macros and replace callsites with corresponding LuaPushNamedFoo. --- rts/Lua/LuaArchive.cpp | 12 +- rts/Lua/LuaFBOs.cpp | 6 +- rts/Lua/LuaFeatureDefs.cpp | 10 +- rts/Lua/LuaFonts.cpp | 4 +- rts/Lua/LuaHandle.cpp | 76 ++++----- rts/Lua/LuaHashString.h | 33 +--- rts/Lua/LuaObjectRendering.cpp | 6 +- rts/Lua/LuaOpenGL.cpp | 80 +++++----- rts/Lua/LuaPathFinder.cpp | 6 +- rts/Lua/LuaRBOs.cpp | 6 +- rts/Lua/LuaScream.cpp | 6 +- rts/Lua/LuaShaders.cpp | 10 +- rts/Lua/LuaSyncedRead.cpp | 278 ++++++++++++++++----------------- rts/Lua/LuaSyncedTable.cpp | 2 +- rts/Lua/LuaUICommand.cpp | 16 +- rts/Lua/LuaUnitDefs.cpp | 56 +++---- rts/Lua/LuaUnsyncedRead.cpp | 2 +- rts/Lua/LuaUtils.cpp | 102 ++++++------ rts/Lua/LuaUtils.h | 67 ++++++++ rts/Lua/LuaVFS.cpp | 90 +++++------ rts/Lua/LuaWeaponDefs.cpp | 72 ++++----- rts/Lua/LuaZip.cpp | 21 +-- rts/System/StringHash.h | 7 +- 23 files changed, 505 insertions(+), 463 deletions(-) diff --git a/rts/Lua/LuaArchive.cpp b/rts/Lua/LuaArchive.cpp index 24ff0727b00..5f85ca9a45b 100644 --- a/rts/Lua/LuaArchive.cpp +++ b/rts/Lua/LuaArchive.cpp @@ -356,21 +356,21 @@ int LuaArchive::GetAvailableAIs(lua_State* L) lua_createtable(L, 0, 3); { for (const auto& luaAIInfoItem: luaAIInfo) { if (luaAIInfoItem.key == SKIRMISH_AI_PROPERTY_SHORT_NAME) { - HSTR_PUSH_STRING(L, "shortName", luaAIInfoItem.GetValueAsString()); + LuaPushNamedString(L, "shortName", luaAIInfoItem.GetValueAsString()); } else if (luaAIInfoItem.key == SKIRMISH_AI_PROPERTY_VERSION) { - HSTR_PUSH_STRING(L, "version", luaAIInfoItem.GetValueAsString()); + LuaPushNamedString(L, "version", luaAIInfoItem.GetValueAsString()); } } - HSTR_PUSH_BOOL(L, "isLuaAI", true); + LuaPushNamedBool(L, "isLuaAI", true); } lua_rawseti(L, -2, ++count); } for (const auto& aiKey: skirmishAIKeys) { lua_createtable(L, 0, 3); { - HSTR_PUSH_STRING(L, "shortName", aiKey.GetShortName()); - HSTR_PUSH_STRING(L, "version", aiKey.GetVersion()); - HSTR_PUSH_BOOL(L, "isLuaAI", false); + LuaPushNamedString(L, "shortName", aiKey.GetShortName()); + LuaPushNamedString(L, "version", aiKey.GetVersion()); + LuaPushNamedBool(L, "isLuaAI", false); } lua_rawseti(L, -2, ++count); } diff --git a/rts/Lua/LuaFBOs.cpp b/rts/Lua/LuaFBOs.cpp index 7269bf5e9a6..f943b5e3703 100644 --- a/rts/Lua/LuaFBOs.cpp +++ b/rts/Lua/LuaFBOs.cpp @@ -64,9 +64,9 @@ bool LuaFBOs::CreateMetatable(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; luaL_newmetatable(L, "FBO"); - HSTR_PUSH_CFUNC(L, "__gc", meta_gc); - HSTR_PUSH_CFUNC(L, "__index", meta_index); - HSTR_PUSH_CFUNC(L, "__newindex", meta_newindex); + LuaPushNamedCFunc(L, "__gc", meta_gc); + LuaPushNamedCFunc(L, "__index", meta_index); + LuaPushNamedCFunc(L, "__newindex", meta_newindex); lua_pop(L, 1); return true; } diff --git a/rts/Lua/LuaFeatureDefs.cpp b/rts/Lua/LuaFeatureDefs.cpp index 54c4a8981bc..2bde825f395 100644 --- a/rts/Lua/LuaFeatureDefs.cpp +++ b/rts/Lua/LuaFeatureDefs.cpp @@ -83,17 +83,17 @@ bool LuaFeatureDefs::PushEntries(lua_State* L) lua_newtable(L); { // the metatable - HSTR_PUSH(L, "__index"); + LuaPushString(L, "__index"); lua_pushlightuserdata(L, (void*)fd); lua_pushcclosure(L, FeatureDefIndex, 1); lua_rawset(L, -3); // closure - HSTR_PUSH(L, "__newindex"); + LuaPushString(L, "__newindex"); lua_pushlightuserdata(L, (void*)fd); lua_pushcclosure(L, FeatureDefNewIndex, 1); lua_rawset(L, -3); - HSTR_PUSH(L, "__metatable"); + LuaPushString(L, "__metatable"); lua_pushlightuserdata(L, (void*)fd); lua_pushcclosure(L, FeatureDefMetatable, 1); lua_rawset(L, -3); @@ -102,11 +102,11 @@ bool LuaFeatureDefs::PushEntries(lua_State* L) lua_setmetatable(L, -2); } - HSTR_PUSH(L, "pairs"); + LuaPushString(L, "pairs"); lua_pushcfunction(L, Pairs); lua_rawset(L, -3); - HSTR_PUSH(L, "next"); + LuaPushString(L, "next"); lua_pushcfunction(L, Next); lua_rawset(L, -3); diff --git a/rts/Lua/LuaFonts.cpp b/rts/Lua/LuaFonts.cpp index 05a0b3d0f55..8bc2acda23f 100644 --- a/rts/Lua/LuaFonts.cpp +++ b/rts/Lua/LuaFonts.cpp @@ -46,8 +46,8 @@ bool LuaFonts::CreateMetatable(lua_State* L) RECOIL_DETAILED_TRACY_ZONE; luaL_newmetatable(L, "Font"); - HSTR_PUSH_CFUNC(L, "__gc", meta_gc); - HSTR_PUSH_CFUNC(L, "__index", meta_index); + LuaPushNamedCFunc(L, "__gc", meta_gc); + LuaPushNamedCFunc(L, "__index", meta_index); LuaPushNamedString(L, "__metatable", "protected metatable"); // push userdata callouts diff --git a/rts/Lua/LuaHandle.cpp b/rts/Lua/LuaHandle.cpp index 6ae29a6f293..195d7aa7a34 100644 --- a/rts/Lua/LuaHandle.cpp +++ b/rts/Lua/LuaHandle.cpp @@ -120,7 +120,7 @@ void CLuaHandle::PushTracebackFuncToRegistry(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; SPRING_LUA_OPEN_LIB(L, luaopen_debug); - HSTR_PUSH(L, "traceback"); + LuaPushString(L, "traceback"); LuaUtils::PushDebugTraceback(L); lua_rawset(L, LUA_REGISTRYINDEX); // We only need the debug.traceback function, the others are unsafe for syncing. @@ -2664,11 +2664,11 @@ bool CLuaHandle::DefaultCommand(const CUnit* unit, return false; if (unit) { - HSTR_PUSH(L, "unit"); + LuaPushString(L, "unit"); lua_pushnumber(L, unit->id); } else if (feature) { - HSTR_PUSH(L, "feature"); + LuaPushString(L, "feature"); lua_pushnumber(L, feature->id); } else { @@ -2679,14 +2679,14 @@ bool CLuaHandle::DefaultCommand(const CUnit* unit, /* FIXME else if (groundPos) { - HSTR_PUSH(L, "ground"); + LuaPushString(L, "ground"); lua_pushnumber(L, groundPos->x); lua_pushnumber(L, groundPos->y); lua_pushnumber(L, groundPos->z); args = 4; } else { - HSTR_PUSH(L, "selection"); + LuaPushString(L, "selection"); args = 1; } */ @@ -3205,10 +3205,10 @@ bool CLuaHandle::KeyPress(int keyCode, int scanCode, bool isRepeat) lua_pushinteger(L, SDL21_keysyms(keyCode)); lua_createtable(L, 0, 4); - HSTR_PUSH_BOOL(L, "alt", !!KeyInput::GetKeyModState(KMOD_ALT)); - HSTR_PUSH_BOOL(L, "ctrl", !!KeyInput::GetKeyModState(KMOD_CTRL)); - HSTR_PUSH_BOOL(L, "meta", !!KeyInput::GetKeyModState(KMOD_GUI)); - HSTR_PUSH_BOOL(L, "shift", !!KeyInput::GetKeyModState(KMOD_SHIFT)); + LuaPushNamedBool(L, "alt", !!KeyInput::GetKeyModState(KMOD_ALT)); + LuaPushNamedBool(L, "ctrl", !!KeyInput::GetKeyModState(KMOD_CTRL)); + LuaPushNamedBool(L, "meta", !!KeyInput::GetKeyModState(KMOD_GUI)); + LuaPushNamedBool(L, "shift", !!KeyInput::GetKeyModState(KMOD_SHIFT)); lua_pushboolean(L, isRepeat); @@ -3268,10 +3268,10 @@ bool CLuaHandle::KeyRelease(int keyCode, int scanCode) lua_pushinteger(L, SDL21_keysyms(keyCode)); lua_createtable(L, 0, 4); - HSTR_PUSH_BOOL(L, "alt", !!KeyInput::GetKeyModState(KMOD_ALT)); - HSTR_PUSH_BOOL(L, "ctrl", !!KeyInput::GetKeyModState(KMOD_CTRL)); - HSTR_PUSH_BOOL(L, "meta", !!KeyInput::GetKeyModState(KMOD_GUI)); - HSTR_PUSH_BOOL(L, "shift", !!KeyInput::GetKeyModState(KMOD_SHIFT)); + LuaPushNamedBool(L, "alt", !!KeyInput::GetKeyModState(KMOD_ALT)); + LuaPushNamedBool(L, "ctrl", !!KeyInput::GetKeyModState(KMOD_CTRL)); + LuaPushNamedBool(L, "meta", !!KeyInput::GetKeyModState(KMOD_GUI)); + LuaPushNamedBool(L, "shift", !!KeyInput::GetKeyModState(KMOD_SHIFT)); CKeySet ks(keyCode); lua_pushsstring(L, ks.GetString(true)); @@ -3815,24 +3815,24 @@ string CLuaHandle::WorldTooltip(const CUnit* unit, int args; if (unit) { - HSTR_PUSH(L, "unit"); + LuaPushString(L, "unit"); lua_pushnumber(L, unit->id); args = 2; } else if (feature) { - HSTR_PUSH(L, "feature"); + LuaPushString(L, "feature"); lua_pushnumber(L, feature->id); args = 2; } else if (groundPos) { - HSTR_PUSH(L, "ground"); + LuaPushString(L, "ground"); lua_pushnumber(L, groundPos->x); lua_pushnumber(L, groundPos->y); lua_pushnumber(L, groundPos->z); args = 4; } else { - HSTR_PUSH(L, "selection"); + LuaPushString(L, "selection"); args = 1; } @@ -3891,7 +3891,7 @@ bool CLuaHandle::MapDrawCmd(int playerID, int type, lua_pushnumber(L, playerID); if (type == MAPDRAW_POINT) { - HSTR_PUSH(L, "point"); + LuaPushString (L, "point"); lua_pushnumber(L, pos0->x); lua_pushnumber(L, pos0->y); lua_pushnumber(L, pos0->z); @@ -3899,7 +3899,7 @@ bool CLuaHandle::MapDrawCmd(int playerID, int type, args = 6; } else if (type == MAPDRAW_LINE) { - HSTR_PUSH(L, "line"); + LuaPushString (L, "line"); lua_pushnumber(L, pos0->x); lua_pushnumber(L, pos0->y); lua_pushnumber(L, pos0->z); @@ -3909,7 +3909,7 @@ bool CLuaHandle::MapDrawCmd(int playerID, int type, args = 8; } else if (type == MAPDRAW_ERASE) { - HSTR_PUSH(L, "erase"); + LuaPushString (L, "erase"); lua_pushnumber(L, pos0->x); lua_pushnumber(L, pos0->y); lua_pushnumber(L, pos0->z); @@ -4251,29 +4251,29 @@ void CLuaHandle::CollectGarbage(bool forced) bool CLuaHandle::AddBasicCalls(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; - HSTR_PUSH(L, "Script"); + LuaPushString(L, "Script"); lua_createtable(L, 0, 17); { - HSTR_PUSH_CFUNC(L, "Kill", KillActiveHandle); - HSTR_PUSH_CFUNC(L, "UpdateCallIn", CallOutUpdateCallIn); - HSTR_PUSH_CFUNC(L, "GetName", CallOutGetName); - HSTR_PUSH_CFUNC(L, "GetSynced", CallOutGetSynced); - HSTR_PUSH_CFUNC(L, "GetFullCtrl", CallOutGetFullCtrl); - HSTR_PUSH_CFUNC(L, "GetFullRead", CallOutGetFullRead); - HSTR_PUSH_CFUNC(L, "GetCtrlTeam", CallOutGetCtrlTeam); - HSTR_PUSH_CFUNC(L, "GetReadTeam", CallOutGetReadTeam); - HSTR_PUSH_CFUNC(L, "GetReadAllyTeam", CallOutGetReadAllyTeam); - HSTR_PUSH_CFUNC(L, "GetSelectTeam", CallOutGetSelectTeam); - HSTR_PUSH_CFUNC(L, "GetGlobal", CallOutGetGlobal); - HSTR_PUSH_CFUNC(L, "GetRegistry", CallOutGetRegistry); - HSTR_PUSH_CFUNC(L, "GetCallInList", CallOutGetCallInList); - HSTR_PUSH_CFUNC(L, "DelayByFrames", CallOutDelayByFrames); - HSTR_PUSH_CFUNC(L, "IsEngineMinVersion", CallOutIsEngineMinVersion); + LuaPushNamedCFunc(L, "Kill", KillActiveHandle); + LuaPushNamedCFunc(L, "UpdateCallIn", CallOutUpdateCallIn); + LuaPushNamedCFunc(L, "GetName", CallOutGetName); + LuaPushNamedCFunc(L, "GetSynced", CallOutGetSynced); + LuaPushNamedCFunc(L, "GetFullCtrl", CallOutGetFullCtrl); + LuaPushNamedCFunc(L, "GetFullRead", CallOutGetFullRead); + LuaPushNamedCFunc(L, "GetCtrlTeam", CallOutGetCtrlTeam); + LuaPushNamedCFunc(L, "GetReadTeam", CallOutGetReadTeam); + LuaPushNamedCFunc(L, "GetReadAllyTeam", CallOutGetReadAllyTeam); + LuaPushNamedCFunc(L, "GetSelectTeam", CallOutGetSelectTeam); + LuaPushNamedCFunc(L, "GetGlobal", CallOutGetGlobal); + LuaPushNamedCFunc(L, "GetRegistry", CallOutGetRegistry); + LuaPushNamedCFunc(L, "GetCallInList", CallOutGetCallInList); + LuaPushNamedCFunc(L, "DelayByFrames", CallOutDelayByFrames); + LuaPushNamedCFunc(L, "IsEngineMinVersion", CallOutIsEngineMinVersion); // special team constants /*** @field Script.NO_ACCESS_TEAM -1 */ - HSTR_PUSH_NUMBER(L, "NO_ACCESS_TEAM", CEventClient::NoAccessTeam); + LuaPushNamedNumber(L, "NO_ACCESS_TEAM", CEventClient::NoAccessTeam); /*** @field Script.ALL_ACCESS_TEAM -2 */ - HSTR_PUSH_NUMBER(L, "ALL_ACCESS_TEAM", CEventClient::AllAccessTeam); + LuaPushNamedNumber(L, "ALL_ACCESS_TEAM", CEventClient::AllAccessTeam); } lua_rawset(L, -3); diff --git a/rts/Lua/LuaHashString.h b/rts/Lua/LuaHashString.h index 16821eccdbe..8de56c5b812 100644 --- a/rts/Lua/LuaHashString.h +++ b/rts/Lua/LuaHashString.h @@ -1,7 +1,6 @@ /* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ -#ifndef LUA_HASH_STRING_H -#define LUA_HASH_STRING_H +#pragma once #include #include @@ -9,7 +8,6 @@ #include "LuaInclude.h" #include "System/StringHash.h" - struct LuaHashString { public: LuaHashString(const char* s): hash(lua_calchash(s, slen = strlen(s))) { @@ -96,31 +94,4 @@ struct LuaHashString { char str[strMaxLen]; uint32_t slen = 0; uint32_t hash = 0; -}; - - -// NOTE: scoped to avoid name conflicts -// NOTE: since all the following are static, if name can change (e.g. within a loop) -// peculiar things will happen. => Only use raw strings (and not variables) in name. - -#define HSTR_PUSH(L, name) \ - { lua_pushhstring(L, COMPILE_TIME_HASH(name), name, sizeof(name) - 1); } - -#define HSTR_PUSH_BOOL(L, name, val) \ - { HSTR_PUSH(L, name); lua_pushboolean(L, val); lua_rawset(L, -3); } - -#define HSTR_PUSH_NUMBER(L, name, val) \ - { HSTR_PUSH(L, name); lua_pushnumber(L, val); lua_rawset(L, -3); } - -#define HSTR_PUSH_STRING(L, name, val) \ - { HSTR_PUSH(L, name); lua_pushsstring(L, val); lua_rawset(L, -3); } - -#define HSTR_PUSH_CSTRING(L, name, val) \ - { HSTR_PUSH(L, name); lua_pushhstring(L, COMPILE_TIME_HASH(val), val, sizeof(val) - 1); lua_rawset(L, -3); } - -#define HSTR_PUSH_CFUNC(L, name, val) \ - { HSTR_PUSH(L, name); lua_pushcfunction(L, val); lua_rawset(L, -3); } - - -#endif // LUA_HASH_STRING_H - +}; \ No newline at end of file diff --git a/rts/Lua/LuaObjectRendering.cpp b/rts/Lua/LuaObjectRendering.cpp index 2d7a73174ac..c26b885be23 100644 --- a/rts/Lua/LuaObjectRendering.cpp +++ b/rts/Lua/LuaObjectRendering.cpp @@ -106,9 +106,9 @@ void LuaObjectRenderingImpl::CreateMatRefMetatable(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; luaL_newmetatable(L, "MatRef"); - HSTR_PUSH_CFUNC(L, "__gc", material_gc); - HSTR_PUSH_CFUNC(L, "__index", material_index); - HSTR_PUSH_CFUNC(L, "__newindex", material_newindex); + LuaPushNamedCFunc(L, "__gc", material_gc); + LuaPushNamedCFunc(L, "__index", material_index); + LuaPushNamedCFunc(L, "__newindex", material_newindex); lua_pop(L, 1); } diff --git a/rts/Lua/LuaOpenGL.cpp b/rts/Lua/LuaOpenGL.cpp index b588056ead2..6c81432df53 100644 --- a/rts/Lua/LuaOpenGL.cpp +++ b/rts/Lua/LuaOpenGL.cpp @@ -4316,14 +4316,14 @@ int LuaOpenGL::TextureInfo(lua_State* L) lua_createtable(L, 0, 5); const auto [xsize, ysize, zsize] = tex.GetSize(); - HSTR_PUSH_NUMBER(L, "xsize", xsize) - HSTR_PUSH_NUMBER(L, "ysize", ysize) - HSTR_PUSH_NUMBER(L, "zsize", zsize) - - HSTR_PUSH_NUMBER(L, "id" , tex.GetTextureID()); - HSTR_PUSH_NUMBER(L, "target", tex.GetTextureTarget()); - // HSTR_PUSH_BOOL(L, "alpha", texInfo.alpha); FIXME - // HSTR_PUSH_NUMBER(L, "type", texInfo.type); + LuaPushNamedNumber(L, "xsize", xsize); + LuaPushNamedNumber(L, "ysize", ysize); + LuaPushNamedNumber(L, "zsize", zsize); + + LuaPushNamedNumber(L, "id" , tex.GetTextureID()); + LuaPushNamedNumber(L, "target", tex.GetTextureTarget()); + // LuaPushNamedBool (L, "alpha", texInfo.alpha); // FIXME + // LuaPushNamedNumber(L, "type", texInfo.type); return 1; } @@ -5845,9 +5845,9 @@ int LuaOpenGL::GetFixedState(lua_State* L) GLint val = 0; glGetIntegerv(key, &val); \ if (toStr) { \ auto strIter = fixedStateEnumToString.find(val); \ - HSTR_PUSH_STRING(L, #key, strIter != fixedStateEnumToString.end() ? strIter->second : fixedStateEnumToStringUnk); \ + LuaPushNamedString(L, #key, strIter != fixedStateEnumToString.end() ? strIter->second : fixedStateEnumToStringUnk); \ } else { \ - HSTR_PUSH_NUMBER(L, #key, val); \ + LuaPushNamedNumber(L, #key, val); \ }; \ } @@ -5891,10 +5891,10 @@ int LuaOpenGL::GetFixedState(lua_State* L) glGetIntegerv(GL_SCISSOR_BOX, rect); lua_createtable(L, 0, 4); - HSTR_PUSH_NUMBER(L, "GL_SCISSOR_BOX_X", rect[0]); - HSTR_PUSH_NUMBER(L, "GL_SCISSOR_BOX_Y", rect[1]); - HSTR_PUSH_NUMBER(L, "GL_SCISSOR_BOX_W", rect[2]); - HSTR_PUSH_NUMBER(L, "GL_SCISSOR_BOX_H", rect[3]); + LuaPushNamedNumber(L, "GL_SCISSOR_BOX_X", rect[0]); + LuaPushNamedNumber(L, "GL_SCISSOR_BOX_Y", rect[1]); + LuaPushNamedNumber(L, "GL_SCISSOR_BOX_W", rect[2]); + LuaPushNamedNumber(L, "GL_SCISSOR_BOX_H", rect[3]); return 2; } break; @@ -5911,10 +5911,10 @@ int LuaOpenGL::GetFixedState(lua_State* L) glGetBooleanv(GL_COLOR_WRITEMASK, mask); lua_createtable(L, 0, 4); - HSTR_PUSH_BOOL(L, "GL_COLOR_WRITEMASK_R", mask[0]); - HSTR_PUSH_BOOL(L, "GL_COLOR_WRITEMASK_G", mask[1]); - HSTR_PUSH_BOOL(L, "GL_COLOR_WRITEMASK_B", mask[2]); - HSTR_PUSH_BOOL(L, "GL_COLOR_WRITEMASK_A", mask[3]); + LuaPushNamedBool(L, "GL_COLOR_WRITEMASK_R", mask[0]); + LuaPushNamedBool(L, "GL_COLOR_WRITEMASK_G", mask[1]); + LuaPushNamedBool(L, "GL_COLOR_WRITEMASK_B", mask[2]); + LuaPushNamedBool(L, "GL_COLOR_WRITEMASK_A", mask[3]); return 1; } break; @@ -5946,7 +5946,7 @@ int LuaOpenGL::GetFixedState(lua_State* L) lua_createtable(L, 0, 2); PushFixedState(GL_ALPHA_TEST_FUNC); - HSTR_PUSH_NUMBER(L, "GL_ALPHA_TEST_REF", alphaRef); + LuaPushNamedNumber(L, "GL_ALPHA_TEST_REF", alphaRef); return 2; } break; @@ -5966,14 +5966,14 @@ int LuaOpenGL::GetFixedState(lua_State* L) glGetFloatv(GL_FOG_END, &fogEnd); lua_createtable(L, 0, 8); - HSTR_PUSH_NUMBER(L, "GL_FOG_COLOR_R", fogColor[0]); - HSTR_PUSH_NUMBER(L, "GL_FOG_COLOR_G", fogColor[1]); - HSTR_PUSH_NUMBER(L, "GL_FOG_COLOR_B", fogColor[2]); - HSTR_PUSH_NUMBER(L, "GL_FOG_COLOR_A", fogColor[3]); + LuaPushNamedNumber(L, "GL_FOG_COLOR_R", fogColor[0]); + LuaPushNamedNumber(L, "GL_FOG_COLOR_G", fogColor[1]); + LuaPushNamedNumber(L, "GL_FOG_COLOR_B", fogColor[2]); + LuaPushNamedNumber(L, "GL_FOG_COLOR_A", fogColor[3]); - HSTR_PUSH_NUMBER(L, "GL_FOG_DENSITY", fogDensity); - HSTR_PUSH_NUMBER(L, "GL_FOG_START", fogStart); - HSTR_PUSH_NUMBER(L, "GL_FOG_END", fogEnd); + LuaPushNamedNumber(L, "GL_FOG_DENSITY", fogDensity); + LuaPushNamedNumber(L, "GL_FOG_START", fogStart); + LuaPushNamedNumber(L, "GL_FOG_END", fogEnd); PushFixedState(GL_FOG_MODE); @@ -6003,8 +6003,8 @@ int LuaOpenGL::GetFixedState(lua_State* L) glGetIntegerv(GL_LINE_STIPPLE_REPEAT, &strippleRepeat); lua_createtable(L, 0, 2); - HSTR_PUSH_NUMBER(L, "GL_LINE_STIPPLE_PATTERN", stripplePattern); - HSTR_PUSH_NUMBER(L, "GL_LINE_STIPPLE_REPEAT", strippleRepeat); + LuaPushNamedNumber(L, "GL_LINE_STIPPLE_PATTERN", stripplePattern); + LuaPushNamedNumber(L, "GL_LINE_STIPPLE_REPEAT", strippleRepeat); return 1; } break; @@ -6024,11 +6024,11 @@ int LuaOpenGL::GetFixedState(lua_State* L) glGetFloatv(GL_POLYGON_OFFSET_UNITS, &offsetDensity); lua_createtable(L, 0, 5); - HSTR_PUSH_BOOL(L, "GL_POLYGON_OFFSET_FILL", glIsEnabled(GL_POLYGON_OFFSET_FILL)); - HSTR_PUSH_BOOL(L, "GL_POLYGON_OFFSET_LINE", glIsEnabled(GL_POLYGON_OFFSET_LINE)); - HSTR_PUSH_BOOL(L, "GL_POLYGON_OFFSET_POINT", glIsEnabled(GL_POLYGON_OFFSET_POINT)); - HSTR_PUSH_NUMBER(L, "GL_POLYGON_OFFSET_FACTOR", offsetFactor); - HSTR_PUSH_NUMBER(L, "GL_POLYGON_OFFSET_UNITS", offsetDensity); + LuaPushNamedBool (L, "GL_POLYGON_OFFSET_FILL", glIsEnabled(GL_POLYGON_OFFSET_FILL)); + LuaPushNamedBool (L, "GL_POLYGON_OFFSET_LINE", glIsEnabled(GL_POLYGON_OFFSET_LINE)); + LuaPushNamedBool (L, "GL_POLYGON_OFFSET_POINT", glIsEnabled(GL_POLYGON_OFFSET_POINT)); + LuaPushNamedNumber(L, "GL_POLYGON_OFFSET_FACTOR", offsetFactor); + LuaPushNamedNumber(L, "GL_POLYGON_OFFSET_UNITS", offsetDensity); return 2; } break; @@ -6045,12 +6045,12 @@ int LuaOpenGL::GetFixedState(lua_State* L) glGetIntegerv(GL_STENCIL_VALUE_MASK, &stencilValueMask); lua_createtable(L, 0, 8); - HSTR_PUSH_NUMBER(L, "GL_STENCIL_WRITEMASK", stencilWriteMask); + LuaPushNamedNumber(L, "GL_STENCIL_WRITEMASK", stencilWriteMask); - HSTR_PUSH_NUMBER(L, "GL_STENCIL_BITS", stencilBits); + LuaPushNamedNumber(L, "GL_STENCIL_BITS", stencilBits); - HSTR_PUSH_NUMBER(L, "GL_STENCIL_VALUE_MASK", stencilValueMask); - HSTR_PUSH_NUMBER(L, "GL_STENCIL_REF", stencilValueMask); + LuaPushNamedNumber(L, "GL_STENCIL_VALUE_MASK", stencilValueMask); + LuaPushNamedNumber(L, "GL_STENCIL_REF", stencilValueMask); PushFixedState(GL_STENCIL_FUNC); @@ -6064,9 +6064,9 @@ int LuaOpenGL::GetFixedState(lua_State* L) GLint stencilBackRef; glGetIntegerv(GL_STENCIL_BACK_REF, &stencilBackRef); - HSTR_PUSH_NUMBER(L, "GL_STENCIL_BACK_WRITEMASK", stencilBackWriteMask); - HSTR_PUSH_NUMBER(L, "GL_STENCIL_BACK_VALUE_MASK", stencilBackValueMask); - HSTR_PUSH_NUMBER(L, "GL_STENCIL_BACK_REF", stencilBackRef); + LuaPushNamedNumber(L, "GL_STENCIL_BACK_WRITEMASK", stencilBackWriteMask); + LuaPushNamedNumber(L, "GL_STENCIL_BACK_VALUE_MASK", stencilBackValueMask); + LuaPushNamedNumber(L, "GL_STENCIL_BACK_REF", stencilBackRef); PushFixedState(GL_STENCIL_BACK_FUNC); } diff --git a/rts/Lua/LuaPathFinder.cpp b/rts/Lua/LuaPathFinder.cpp index 763a53f123f..9e9c3dc24b8 100644 --- a/rts/Lua/LuaPathFinder.cpp +++ b/rts/Lua/LuaPathFinder.cpp @@ -201,9 +201,9 @@ static int path_gc(lua_State* L) static void CreatePathMetatable(lua_State* L) { luaL_newmetatable(L, "Path"); - HSTR_PUSH_CFUNC(L, "__gc", path_gc); - HSTR_PUSH_CFUNC(L, "__index", path_index); - HSTR_PUSH_CFUNC(L, "__newindex", path_newindex); + LuaPushNamedCFunc(L, "__gc", path_gc); + LuaPushNamedCFunc(L, "__index", path_index); + LuaPushNamedCFunc(L, "__newindex", path_newindex); lua_pop(L, 1); } diff --git a/rts/Lua/LuaRBOs.cpp b/rts/Lua/LuaRBOs.cpp index 520f53796d6..959f1bd7078 100644 --- a/rts/Lua/LuaRBOs.cpp +++ b/rts/Lua/LuaRBOs.cpp @@ -41,9 +41,9 @@ bool LuaRBOs::PushEntries(lua_State* L) bool LuaRBOs::CreateMetatable(lua_State* L) { luaL_newmetatable(L, "RBO"); - HSTR_PUSH_CFUNC(L, "__gc", meta_gc); - HSTR_PUSH_CFUNC(L, "__index", meta_index); - HSTR_PUSH_CFUNC(L, "__newindex", meta_newindex); + LuaPushNamedCFunc(L, "__gc", meta_gc); + LuaPushNamedCFunc(L, "__index", meta_index); + LuaPushNamedCFunc(L, "__newindex", meta_newindex); lua_pop(L, 1); return true; } diff --git a/rts/Lua/LuaScream.cpp b/rts/Lua/LuaScream.cpp index 98065386188..d733242b020 100644 --- a/rts/Lua/LuaScream.cpp +++ b/rts/Lua/LuaScream.cpp @@ -22,9 +22,9 @@ bool LuaScream::PushEntries(lua_State* L) bool LuaScream::CreateMetatable(lua_State* L) { luaL_newmetatable(L, "Scream"); - HSTR_PUSH_CFUNC(L, "__gc", meta_gc); - HSTR_PUSH_CFUNC(L, "__index", meta_index); - HSTR_PUSH_CFUNC(L, "__newindex", meta_newindex); + LuaPushNamedCFunc(L, "__gc", meta_gc); + LuaPushNamedCFunc(L, "__index", meta_index); + LuaPushNamedCFunc(L, "__newindex", meta_newindex); lua_pop(L, 1); return true; } diff --git a/rts/Lua/LuaShaders.cpp b/rts/Lua/LuaShaders.cpp index 5109e5d64c3..5d17d17c6d9 100644 --- a/rts/Lua/LuaShaders.cpp +++ b/rts/Lua/LuaShaders.cpp @@ -953,11 +953,11 @@ int LuaShaders::GetActiveUniforms(lua_State* L) GLint i = 0; for (const auto& [name, au] : prog->activeUniforms) { lua_createtable(L, 0, 5); { - HSTR_PUSH_STRING(L, "name" , name); - HSTR_PUSH_STRING(L, "type" , UniformTypeString(au.type)); - HSTR_PUSH_NUMBER(L, "length" , name.size()); - HSTR_PUSH_NUMBER(L, "size" , au.size); - HSTR_PUSH_NUMBER(L, "location", prog->activeUniformLocations.at(name).location); + LuaPushNamedString(L, "name" , name); + LuaPushNamedString(L, "type" , UniformTypeString(au.type)); + LuaPushNamedNumber(L, "length" , name.size()); + LuaPushNamedNumber(L, "size" , au.size); + LuaPushNamedNumber(L, "location", prog->activeUniformLocations.at(name).location); } lua_rawseti(L, -2, i + 1); ++i; diff --git a/rts/Lua/LuaSyncedRead.cpp b/rts/Lua/LuaSyncedRead.cpp index 3983a71f3ff..1af92101752 100644 --- a/rts/Lua/LuaSyncedRead.cpp +++ b/rts/Lua/LuaSyncedRead.cpp @@ -2080,35 +2080,35 @@ int LuaSyncedRead::GetTeamStatsHistory(lua_State* L) // the `stats.frame` var indicates the frame when a new entry needs to get added, // for the most recent stats entry this lies obviously in the future, // so we just output the current frame here - HSTR_PUSH_NUMBER(L, "time", gs->GetLuaSimFrame() / GAME_SPEED); - HSTR_PUSH_NUMBER(L, "frame", gs->GetLuaSimFrame()); + LuaPushNamedNumber(L, "time", gs->GetLuaSimFrame() / GAME_SPEED); + LuaPushNamedNumber(L, "frame", gs->GetLuaSimFrame()); } else { - HSTR_PUSH_NUMBER(L, "time", stats.frame / GAME_SPEED); - HSTR_PUSH_NUMBER(L, "frame", stats.frame); + LuaPushNamedNumber(L, "time", stats.frame / GAME_SPEED); + LuaPushNamedNumber(L, "frame", stats.frame); } - HSTR_PUSH_NUMBER(L, "metalUsed", stats.metalUsed); - HSTR_PUSH_NUMBER(L, "metalProduced", stats.metalProduced); - HSTR_PUSH_NUMBER(L, "metalExcess", stats.metalExcess); - HSTR_PUSH_NUMBER(L, "metalReceived", stats.metalReceived); - HSTR_PUSH_NUMBER(L, "metalSent", stats.metalSent); - - HSTR_PUSH_NUMBER(L, "energyUsed", stats.energyUsed); - HSTR_PUSH_NUMBER(L, "energyProduced", stats.energyProduced); - HSTR_PUSH_NUMBER(L, "energyExcess", stats.energyExcess); - HSTR_PUSH_NUMBER(L, "energyReceived", stats.energyReceived); - HSTR_PUSH_NUMBER(L, "energySent", stats.energySent); - - HSTR_PUSH_NUMBER(L, "damageDealt", stats.damageDealt); - HSTR_PUSH_NUMBER(L, "damageReceived", stats.damageReceived); - - HSTR_PUSH_NUMBER(L, "unitsProduced", stats.unitsProduced); - HSTR_PUSH_NUMBER(L, "unitsDied", stats.unitsDied); - HSTR_PUSH_NUMBER(L, "unitsReceived", stats.unitsReceived); - HSTR_PUSH_NUMBER(L, "unitsSent", stats.unitsSent); - HSTR_PUSH_NUMBER(L, "unitsCaptured", stats.unitsCaptured); - HSTR_PUSH_NUMBER(L, "unitsOutCaptured", stats.unitsOutCaptured); - HSTR_PUSH_NUMBER(L, "unitsKilled", stats.unitsKilled); + LuaPushNamedNumber(L, "metalUsed", stats.metalUsed); + LuaPushNamedNumber(L, "metalProduced", stats.metalProduced); + LuaPushNamedNumber(L, "metalExcess", stats.metalExcess); + LuaPushNamedNumber(L, "metalReceived", stats.metalReceived); + LuaPushNamedNumber(L, "metalSent", stats.metalSent); + + LuaPushNamedNumber(L, "energyUsed", stats.energyUsed); + LuaPushNamedNumber(L, "energyProduced", stats.energyProduced); + LuaPushNamedNumber(L, "energyExcess", stats.energyExcess); + LuaPushNamedNumber(L, "energyReceived", stats.energyReceived); + LuaPushNamedNumber(L, "energySent", stats.energySent); + + LuaPushNamedNumber(L, "damageDealt", stats.damageDealt); + LuaPushNamedNumber(L, "damageReceived", stats.damageReceived); + + LuaPushNamedNumber(L, "unitsProduced", stats.unitsProduced); + LuaPushNamedNumber(L, "unitsDied", stats.unitsDied); + LuaPushNamedNumber(L, "unitsReceived", stats.unitsReceived); + LuaPushNamedNumber(L, "unitsSent", stats.unitsSent); + LuaPushNamedNumber(L, "unitsCaptured", stats.unitsCaptured); + LuaPushNamedNumber(L, "unitsOutCaptured", stats.unitsOutCaptured); + LuaPushNamedNumber(L, "unitsKilled", stats.unitsKilled); } lua_rawseti(L, -2, count++); } @@ -2311,8 +2311,8 @@ int LuaSyncedRead::GetAIInfo(lua_State* L) // no unsynced Skirmish AI info for synchronized scripts if (CLuaHandle::GetHandleSynced(L)) { - HSTR_PUSH(L, "SYNCED_NOSHORTNAME"); - HSTR_PUSH(L, "SYNCED_NOVERSION"); + LuaPushString(L, "SYNCED_NOSHORTNAME"); + LuaPushString(L, "SYNCED_NOVERSION"); lua_newtable(L); } else if (skirmishAIHandler.IsLocalSkirmishAI(skirmishAIId)) { lua_pushsstring(L, aiData->shortName); @@ -2326,8 +2326,8 @@ int LuaSyncedRead::GetAIInfo(lua_State* L) lua_rawset(L, -3); } } else { - HSTR_PUSH(L, "UNKNOWN"); - HSTR_PUSH(L, "UNKNOWN"); + LuaPushString(L, "UNKNOWN"); + LuaPushString(L, "UNKNOWN"); lua_newtable(L); } numVals += 3; @@ -2631,7 +2631,7 @@ int LuaSyncedRead::GetTeamUnitsSorted(lua_State* L) } if (!gtuObjectIDs.empty()) { - HSTR_PUSH(L, "unknown"); + LuaPushString(L, "unknown"); defCount += 1; unitCount = 1; @@ -2725,7 +2725,7 @@ int LuaSyncedRead::GetTeamUnitsCounts(lua_State* L) defCount++; } if (unknownCount > 0) { - HSTR_PUSH_NUMBER(L, "unknown", unknownCount); + LuaPushNamedNumber(L, "unknown", unknownCount); defCount++; } @@ -3738,16 +3738,16 @@ int LuaSyncedRead::GetUnitStates(lua_State* L) lua_createtable(L, 0, 9); { - HSTR_PUSH_NUMBER(L, "firestate", unit->fireState); - HSTR_PUSH_NUMBER(L, "movestate", unit->moveState); - HSTR_PUSH_NUMBER(L, "autorepairlevel", (mCAI != nullptr)? mCAI->repairBelowHealth: -1.0f); + LuaPushNamedNumber(L, "firestate", unit->fireState); + LuaPushNamedNumber(L, "movestate", unit->moveState); + LuaPushNamedNumber(L, "autorepairlevel", (mCAI != nullptr)? mCAI->repairBelowHealth: -1.0f); } if (binState) { - HSTR_PUSH_BOOL(L, "repeat", unit->commandAI->repeatOrders); - HSTR_PUSH_BOOL(L, "cloak", unit->wantCloak); - HSTR_PUSH_BOOL(L, "active", unit->activated); - HSTR_PUSH_BOOL(L, "trajectory", unit->useHighTrajectory); + LuaPushNamedBool(L, "repeat", unit->commandAI->repeatOrders); + LuaPushNamedBool(L, "cloak", unit->wantCloak); + LuaPushNamedBool(L, "active", unit->activated); + LuaPushNamedBool(L, "trajectory", unit->useHighTrajectory); } if (amtState) { @@ -3755,14 +3755,14 @@ int LuaSyncedRead::GetUnitStates(lua_State* L) const CStrafeAirMoveType* sAMT = nullptr; if ((hAMT = dynamic_cast(mt)) != nullptr) { - HSTR_PUSH_BOOL(L, "autoland", hAMT->autoLand); - HSTR_PUSH_BOOL(L, "loopbackattack", false); + LuaPushNamedBool(L, "autoland", hAMT->autoLand); + LuaPushNamedBool(L, "loopbackattack", false); return 1; } if ((sAMT = dynamic_cast(mt)) != nullptr) { - HSTR_PUSH_BOOL(L, "autoland", sAMT->autoLand); - HSTR_PUSH_BOOL(L, "loopbackattack", sAMT->loopbackAttack); + LuaPushNamedBool(L, "autoland", sAMT->autoLand); + LuaPushNamedBool(L, "loopbackattack", sAMT->loopbackAttack); return 1; } } @@ -5910,17 +5910,17 @@ int LuaSyncedRead::GetUnitDefDimensions(lua_State* L) const S3DModel& m = *model; const float3& mid = model->relMidPos; lua_createtable(L, 0, 11); - HSTR_PUSH_NUMBER(L, "height", m.height); - HSTR_PUSH_NUMBER(L, "radius", m.radius); - HSTR_PUSH_NUMBER(L, "midx", mid.x); - HSTR_PUSH_NUMBER(L, "minx", m.mins.x); - HSTR_PUSH_NUMBER(L, "maxx", m.maxs.x); - HSTR_PUSH_NUMBER(L, "midy", mid.y); - HSTR_PUSH_NUMBER(L, "miny", m.mins.y); - HSTR_PUSH_NUMBER(L, "maxy", m.maxs.y); - HSTR_PUSH_NUMBER(L, "midz", mid.z); - HSTR_PUSH_NUMBER(L, "minz", m.mins.z); - HSTR_PUSH_NUMBER(L, "maxz", m.maxs.z); + LuaPushNamedNumber(L, "height", m.height); + LuaPushNamedNumber(L, "radius", m.radius); + LuaPushNamedNumber(L, "midx", mid.x); + LuaPushNamedNumber(L, "minx", m.mins.x); + LuaPushNamedNumber(L, "maxx", m.maxs.x); + LuaPushNamedNumber(L, "midy", mid.y); + LuaPushNamedNumber(L, "miny", m.mins.y); + LuaPushNamedNumber(L, "maxy", m.maxs.y); + LuaPushNamedNumber(L, "midz", mid.z); + LuaPushNamedNumber(L, "minz", m.mins.z); + LuaPushNamedNumber(L, "maxz", m.maxs.z); return 1; } @@ -5968,49 +5968,49 @@ int LuaSyncedRead::GetUnitMoveTypeData(lua_State* L) AMoveType* amt = unit->moveType; lua_createtable(L, 0, 26); - HSTR_PUSH_NUMBER(L, "maxSpeed", amt->GetMaxSpeed() * GAME_SPEED); - HSTR_PUSH_NUMBER(L, "maxWantedSpeed", amt->GetMaxWantedSpeed() * GAME_SPEED); - HSTR_PUSH_NUMBER(L, "goalx", amt->goalPos.x); - HSTR_PUSH_NUMBER(L, "goaly", amt->goalPos.y); - HSTR_PUSH_NUMBER(L, "goalz", amt->goalPos.z); + LuaPushNamedNumber(L, "maxSpeed", amt->GetMaxSpeed() * GAME_SPEED); + LuaPushNamedNumber(L, "maxWantedSpeed", amt->GetMaxWantedSpeed() * GAME_SPEED); + LuaPushNamedNumber(L, "goalx", amt->goalPos.x); + LuaPushNamedNumber(L, "goaly", amt->goalPos.y); + LuaPushNamedNumber(L, "goalz", amt->goalPos.z); switch (amt->progressState) { case AMoveType::Done: - HSTR_PUSH_CSTRING(L, "progressState", "done"); + LuaPushNamedString(L, "progressState", "done"); break; case AMoveType::Active: - HSTR_PUSH_CSTRING(L, "progressState", "active"); + LuaPushNamedString(L, "progressState", "active"); break; case AMoveType::Failed: - HSTR_PUSH_CSTRING(L, "progressState", "failed"); + LuaPushNamedString(L, "progressState", "failed"); break; } const CGroundMoveType* groundmt = dynamic_cast(unit->moveType); if (groundmt != nullptr) { - HSTR_PUSH_CSTRING(L, "name", "ground"); + LuaPushNamedString(L, "name", "ground"); - HSTR_PUSH_NUMBER(L, "turnRate", groundmt->GetTurnRate()); - HSTR_PUSH_NUMBER(L, "accRate", groundmt->GetAccRate()); - HSTR_PUSH_NUMBER(L, "decRate", groundmt->GetDecRate()); + LuaPushNamedNumber(L, "turnRate", groundmt->GetTurnRate()); + LuaPushNamedNumber(L, "accRate", groundmt->GetAccRate()); + LuaPushNamedNumber(L, "decRate", groundmt->GetDecRate()); - HSTR_PUSH_NUMBER(L, "maxReverseSpeed", groundmt->GetMaxReverseSpeed() * GAME_SPEED); - HSTR_PUSH_NUMBER(L, "wantedSpeed", groundmt->GetWantedSpeed() * GAME_SPEED); - HSTR_PUSH_NUMBER(L, "currentSpeed", groundmt->GetCurrentSpeed() * GAME_SPEED); + LuaPushNamedNumber(L, "maxReverseSpeed", groundmt->GetMaxReverseSpeed() * GAME_SPEED); + LuaPushNamedNumber(L, "wantedSpeed", groundmt->GetWantedSpeed() * GAME_SPEED); + LuaPushNamedNumber(L, "currentSpeed", groundmt->GetCurrentSpeed() * GAME_SPEED); - HSTR_PUSH_NUMBER(L, "goalRadius", groundmt->GetGoalRadius()); + LuaPushNamedNumber(L, "goalRadius", groundmt->GetGoalRadius()); - HSTR_PUSH_NUMBER(L, "currwaypointx", (groundmt->GetCurrWayPoint()).x); - HSTR_PUSH_NUMBER(L, "currwaypointy", (groundmt->GetCurrWayPoint()).y); - HSTR_PUSH_NUMBER(L, "currwaypointz", (groundmt->GetCurrWayPoint()).z); - HSTR_PUSH_NUMBER(L, "nextwaypointx", (groundmt->GetNextWayPoint()).x); - HSTR_PUSH_NUMBER(L, "nextwaypointy", (groundmt->GetNextWayPoint()).y); - HSTR_PUSH_NUMBER(L, "nextwaypointz", (groundmt->GetNextWayPoint()).z); + LuaPushNamedNumber(L, "currwaypointx", (groundmt->GetCurrWayPoint()).x); + LuaPushNamedNumber(L, "currwaypointy", (groundmt->GetCurrWayPoint()).y); + LuaPushNamedNumber(L, "currwaypointz", (groundmt->GetCurrWayPoint()).z); + LuaPushNamedNumber(L, "nextwaypointx", (groundmt->GetNextWayPoint()).x); + LuaPushNamedNumber(L, "nextwaypointy", (groundmt->GetNextWayPoint()).y); + LuaPushNamedNumber(L, "nextwaypointz", (groundmt->GetNextWayPoint()).z); - HSTR_PUSH_NUMBER(L, "requestedSpeed", 0.0f); + LuaPushNamedNumber(L, "requestedSpeed", 0.0f); - HSTR_PUSH_NUMBER(L, "pathFailures", 0); + LuaPushNamedNumber(L, "pathFailures", 0); return 1; } @@ -6018,62 +6018,62 @@ int LuaSyncedRead::GetUnitMoveTypeData(lua_State* L) const CHoverAirMoveType* hAMT = dynamic_cast(unit->moveType); if (hAMT != nullptr) { - HSTR_PUSH_CSTRING(L, "name", "gunship"); + LuaPushNamedString(L, "name", "gunship"); - HSTR_PUSH_NUMBER(L, "wantedHeight", hAMT->wantedHeight); - HSTR_PUSH_BOOL(L, "collide", hAMT->collide); - HSTR_PUSH_BOOL(L, "useSmoothMesh", hAMT->useSmoothMesh); + LuaPushNamedNumber(L, "wantedHeight", hAMT->wantedHeight); + LuaPushNamedBool(L, "collide", hAMT->collide); + LuaPushNamedBool(L, "useSmoothMesh", hAMT->useSmoothMesh); switch (hAMT->aircraftState) { case AAirMoveType::AIRCRAFT_LANDED: - HSTR_PUSH_CSTRING(L, "aircraftState", "landed"); + LuaPushNamedString(L, "aircraftState", "landed"); break; case AAirMoveType::AIRCRAFT_FLYING: - HSTR_PUSH_CSTRING(L, "aircraftState", "flying"); + LuaPushNamedString(L, "aircraftState", "flying"); break; case AAirMoveType::AIRCRAFT_LANDING: - HSTR_PUSH_CSTRING(L, "aircraftState", "landing"); + LuaPushNamedString(L, "aircraftState", "landing"); break; case AAirMoveType::AIRCRAFT_CRASHING: - HSTR_PUSH_CSTRING(L, "aircraftState", "crashing"); + LuaPushNamedString(L, "aircraftState", "crashing"); break; case AAirMoveType::AIRCRAFT_TAKEOFF: - HSTR_PUSH_CSTRING(L, "aircraftState", "takeoff"); + LuaPushNamedString(L, "aircraftState", "takeoff"); break; case AAirMoveType::AIRCRAFT_HOVERING: - HSTR_PUSH_CSTRING(L, "aircraftState", "hovering"); + LuaPushNamedString(L, "aircraftState", "hovering"); break; }; switch (hAMT->flyState) { case CHoverAirMoveType::FLY_CRUISING: - HSTR_PUSH_CSTRING(L, "flyState", "cruising"); + LuaPushNamedString(L, "flyState", "cruising"); break; case CHoverAirMoveType::FLY_CIRCLING: - HSTR_PUSH_CSTRING(L, "flyState", "circling"); + LuaPushNamedString(L, "flyState", "circling"); break; case CHoverAirMoveType::FLY_ATTACKING: - HSTR_PUSH_CSTRING(L, "flyState", "attacking"); + LuaPushNamedString(L, "flyState", "attacking"); break; case CHoverAirMoveType::FLY_LANDING: - HSTR_PUSH_CSTRING(L, "flyState", "landing"); + LuaPushNamedString(L, "flyState", "landing"); break; } - HSTR_PUSH_NUMBER(L, "goalDistance", hAMT->goalDistance); + LuaPushNamedNumber(L, "goalDistance", hAMT->goalDistance); - HSTR_PUSH_BOOL(L, "bankingAllowed", hAMT->bankingAllowed); - HSTR_PUSH_NUMBER(L, "currentBank", hAMT->currentBank); - HSTR_PUSH_NUMBER(L, "currentPitch", hAMT->currentPitch); + LuaPushNamedBool (L, "bankingAllowed", hAMT->bankingAllowed); + LuaPushNamedNumber(L, "currentBank", hAMT->currentBank); + LuaPushNamedNumber(L, "currentPitch", hAMT->currentPitch); - HSTR_PUSH_NUMBER(L, "turnRate", hAMT->turnRate); - HSTR_PUSH_NUMBER(L, "accRate", hAMT->accRate); - HSTR_PUSH_NUMBER(L, "decRate", hAMT->decRate); - HSTR_PUSH_NUMBER(L, "altitudeRate", hAMT->altitudeRate); + LuaPushNamedNumber(L, "turnRate", hAMT->turnRate); + LuaPushNamedNumber(L, "accRate", hAMT->accRate); + LuaPushNamedNumber(L, "decRate", hAMT->decRate); + LuaPushNamedNumber(L, "altitudeRate", hAMT->altitudeRate); - HSTR_PUSH_NUMBER(L, "brakeDistance", -1.0f); // DEPRECATED - HSTR_PUSH_BOOL(L, "dontLand", hAMT->GetAllowLanding()); - HSTR_PUSH_NUMBER(L, "maxDrift", hAMT->maxDrift); + LuaPushNamedNumber(L, "brakeDistance", -1.0f); // DEPRECATED + LuaPushNamedBool (L, "dontLand", hAMT->GetAllowLanding()); + LuaPushNamedNumber(L, "maxDrift", hAMT->maxDrift); return 1; } @@ -6081,42 +6081,42 @@ int LuaSyncedRead::GetUnitMoveTypeData(lua_State* L) const CStrafeAirMoveType* sAMT = dynamic_cast(unit->moveType); if (sAMT != nullptr) { - HSTR_PUSH_CSTRING(L, "name", "airplane"); + LuaPushNamedString(L, "name", "airplane"); switch (sAMT->aircraftState) { case AAirMoveType::AIRCRAFT_LANDED: - HSTR_PUSH_CSTRING(L, "aircraftState", "landed"); + LuaPushNamedString(L, "aircraftState", "landed"); break; case AAirMoveType::AIRCRAFT_FLYING: - HSTR_PUSH_CSTRING(L, "aircraftState", "flying"); + LuaPushNamedString(L, "aircraftState", "flying"); break; case AAirMoveType::AIRCRAFT_LANDING: - HSTR_PUSH_CSTRING(L, "aircraftState", "landing"); + LuaPushNamedString(L, "aircraftState", "landing"); break; case AAirMoveType::AIRCRAFT_CRASHING: - HSTR_PUSH_CSTRING(L, "aircraftState", "crashing"); + LuaPushNamedString(L, "aircraftState", "crashing"); break; case AAirMoveType::AIRCRAFT_TAKEOFF: - HSTR_PUSH_CSTRING(L, "aircraftState", "takeoff"); + LuaPushNamedString(L, "aircraftState", "takeoff"); break; case AAirMoveType::AIRCRAFT_HOVERING: - HSTR_PUSH_CSTRING(L, "aircraftState", "hovering"); + LuaPushNamedString(L, "aircraftState", "hovering"); break; }; - HSTR_PUSH_NUMBER(L, "wantedHeight", sAMT->wantedHeight); - HSTR_PUSH_BOOL(L, "collide", sAMT->collide); - HSTR_PUSH_BOOL(L, "useSmoothMesh", sAMT->useSmoothMesh); + LuaPushNamedNumber(L, "wantedHeight", sAMT->wantedHeight); + LuaPushNamedBool (L, "collide", sAMT->collide); + LuaPushNamedBool (L, "useSmoothMesh", sAMT->useSmoothMesh); - HSTR_PUSH_NUMBER(L, "myGravity", sAMT->myGravity); + LuaPushNamedNumber(L, "myGravity", sAMT->myGravity); - HSTR_PUSH_NUMBER(L, "maxBank", sAMT->maxBank); - HSTR_PUSH_NUMBER(L, "maxPitch", sAMT->maxBank); - HSTR_PUSH_NUMBER(L, "turnRadius", sAMT->turnRadius); + LuaPushNamedNumber(L, "maxBank", sAMT->maxBank); + LuaPushNamedNumber(L, "maxPitch", sAMT->maxBank); + LuaPushNamedNumber(L, "turnRadius", sAMT->turnRadius); - HSTR_PUSH_NUMBER(L, "maxAcc", sAMT->accRate); - HSTR_PUSH_NUMBER(L, "maxAileron", sAMT->maxAileron); - HSTR_PUSH_NUMBER(L, "maxElevator", sAMT->maxElevator); - HSTR_PUSH_NUMBER(L, "maxRudder", sAMT->maxRudder); + LuaPushNamedNumber(L, "maxAcc", sAMT->accRate); + LuaPushNamedNumber(L, "maxAileron", sAMT->maxAileron); + LuaPushNamedNumber(L, "maxElevator", sAMT->maxElevator); + LuaPushNamedNumber(L, "maxRudder", sAMT->maxRudder); return 1; } @@ -6124,18 +6124,18 @@ int LuaSyncedRead::GetUnitMoveTypeData(lua_State* L) const CStaticMoveType* staticmt = dynamic_cast(unit->moveType); if (staticmt != nullptr) { - HSTR_PUSH_CSTRING(L, "name", "static"); + LuaPushNamedString(L, "name", "static"); return 1; } const CScriptMoveType* scriptmt = dynamic_cast(unit->moveType); if (scriptmt != nullptr) { - HSTR_PUSH_CSTRING(L, "name", "script"); + LuaPushNamedString(L, "name", "script"); return 1; } - HSTR_PUSH_CSTRING(L, "name", "unknown"); + LuaPushNamedString(L, "name", "unknown"); return 1; } @@ -6154,14 +6154,14 @@ static void PackCommand(lua_State* L, const Command& cmd) { lua_createtable(L, 0, 4); - HSTR_PUSH_NUMBER(L, "id", cmd.GetID()); + LuaPushNamedNumber(L, "id", cmd.GetID()); // t["params"] = {[1] = param1, ...} LuaUtils::PushCommandParamsTable(L, cmd, true); // t["options"] = {key1 = val1, ...} LuaUtils::PushCommandOptionsTable(L, cmd, true); - HSTR_PUSH_NUMBER(L, "tag", cmd.GetTag()); + LuaPushNamedNumber(L, "tag", cmd.GetTag()); } @@ -7809,7 +7809,7 @@ int LuaSyncedRead::GetGroundBlocked(lua_State* L) const CFeature* feature = dynamic_cast(s); if (feature != nullptr) { if (LuaUtils::IsFeatureVisible(L, feature)) { - HSTR_PUSH(L, "feature"); + LuaPushString(L, "feature"); lua_pushnumber(L, feature->id); return 2; } @@ -7820,7 +7820,7 @@ int LuaSyncedRead::GetGroundBlocked(lua_State* L) const CUnit* unit = dynamic_cast(s); if (unit != nullptr) { if (CLuaHandle::GetHandleFullRead(L) || (unit->losStatus[CLuaHandle::GetHandleReadAllyTeam(L)] & LOS_INLOS)) { - HSTR_PUSH(L, "unit"); + LuaPushString(L, "unit"); lua_pushnumber(L, unit->id); return 2; } @@ -8293,13 +8293,13 @@ int LuaSyncedRead::GetUnitLosState(lua_State* L) lua_createtable(L, 0, 3); if (losStatus & LOS_INLOS) { - HSTR_PUSH_BOOL(L, "los", true); + LuaPushNamedBool(L, "los", true); } if (losStatus & LOS_INRADAR) { - HSTR_PUSH_BOOL(L, "radar", true); + LuaPushNamedBool(L, "radar", true); } if ((losStatus & LOS_INLOS) || isTyped) { - HSTR_PUSH_BOOL(L, "typed", true); + LuaPushNamedBool(L, "typed", true); } return 1; } @@ -8541,10 +8541,10 @@ static int GetSolidObjectPieceList(lua_State* L, const CSolidObject* o) static int GetSolidObjectPieceInfoHelper(lua_State* L, const S3DModelPiece& op) { lua_createtable(L, 0, 7); - HSTR_PUSH_STRING(L, "name", op.name); - HSTR_PUSH_STRING(L, "parent", ((op.parent != nullptr) ? op.parent->name : "[null]")); + LuaPushNamedString(L, "name", op.name); + LuaPushNamedString(L, "parent", ((op.parent != nullptr) ? op.parent->name : "[null]")); - HSTR_PUSH(L, "children"); + LuaPushString(L, "children"); lua_createtable(L, op.children.size(), 0); for (size_t c = 0; c < op.children.size(); c++) { lua_pushsstring(L, op.children[c]->name); @@ -8552,11 +8552,9 @@ static int GetSolidObjectPieceInfoHelper(lua_State* L, const S3DModelPiece& op) } lua_rawset(L, -3); - HSTR_PUSH(L, "isEmpty"); - lua_pushboolean(L, !op.HasGeometryData()); - lua_rawset(L, -3); + LuaPushNamedBool(L, "isEmpty", !op.HasGeometryData()); - HSTR_PUSH(L, "min"); + LuaPushString(L, "min"); lua_createtable(L, 3, 0); { lua_pushnumber(L, op.mins.x); lua_rawseti(L, -2, 1); lua_pushnumber(L, op.mins.y); lua_rawseti(L, -2, 2); @@ -8564,7 +8562,7 @@ static int GetSolidObjectPieceInfoHelper(lua_State* L, const S3DModelPiece& op) } lua_rawset(L, -3); - HSTR_PUSH(L, "max"); + LuaPushString(L, "max"); lua_createtable(L, 3, 0); { lua_pushnumber(L, op.maxs.x); lua_rawseti(L, -2, 1); lua_pushnumber(L, op.maxs.y); lua_rawseti(L, -2, 2); @@ -8572,7 +8570,7 @@ static int GetSolidObjectPieceInfoHelper(lua_State* L, const S3DModelPiece& op) } lua_rawset(L, -3); - HSTR_PUSH(L, "offset"); + LuaPushString(L, "offset"); lua_createtable(L, 3, 0); { lua_pushnumber(L, op.offset.x); lua_rawseti(L, -2, 1); lua_pushnumber(L, op.offset.y); lua_rawseti(L, -2, 2); diff --git a/rts/Lua/LuaSyncedTable.cpp b/rts/Lua/LuaSyncedTable.cpp index a1d76c21d58..27452aacc65 100644 --- a/rts/Lua/LuaSyncedTable.cpp +++ b/rts/Lua/LuaSyncedTable.cpp @@ -91,7 +91,7 @@ static int SyncTableMetatable(lua_State* L) */ bool LuaSyncedTable::PushEntries(lua_State* L) { - HSTR_PUSH(L, "SYNCED"); + LuaPushString(L, "SYNCED"); lua_newtable(L); { // the proxy table lua_createtable(L, 0, 3); { // the metatable diff --git a/rts/Lua/LuaUICommand.cpp b/rts/Lua/LuaUICommand.cpp index 2258311f49d..b97e0b53ed5 100644 --- a/rts/Lua/LuaUICommand.cpp +++ b/rts/Lua/LuaUICommand.cpp @@ -40,20 +40,20 @@ int LuaUICommand::GetUICommands(lua_State* L) const ISyncedActionExecutor* exec = pair.second; lua_createtable(L, 0, 4); - HSTR_PUSH_STRING(L, "command", exec->GetCommand()); - HSTR_PUSH_STRING(L, "description", exec->GetDescription()); - HSTR_PUSH_BOOL(L, "synced", exec->IsSynced()); - HSTR_PUSH_BOOL(L, "cheat", exec->IsCheatRequired()); + LuaPushNamedString(L, "command", exec->GetCommand()); + LuaPushNamedString(L, "description", exec->GetDescription()); + LuaPushNamedBool(L, "synced", exec->IsSynced()); + LuaPushNamedBool(L, "cheat", exec->IsCheatRequired()); lua_rawseti(L, -2, count++); } for (const auto& pair: unsyncedExecutors) { const IUnsyncedActionExecutor* exec = pair.second; lua_createtable(L, 0, 4); - HSTR_PUSH_STRING(L, "command", exec->GetCommand()); - HSTR_PUSH_STRING(L, "description", exec->GetDescription()); - HSTR_PUSH_BOOL(L, "synced", exec->IsSynced()); - HSTR_PUSH_BOOL(L, "cheat", exec->IsCheatRequired()); + LuaPushNamedString(L, "command", exec->GetCommand()); + LuaPushNamedString(L, "description", exec->GetDescription()); + LuaPushNamedBool(L, "synced", exec->IsSynced()); + LuaPushNamedBool(L, "cheat", exec->IsCheatRequired()); lua_rawseti(L, -2, count++); } return 1; diff --git a/rts/Lua/LuaUnitDefs.cpp b/rts/Lua/LuaUnitDefs.cpp index 3818b78a7e5..21568ce42b9 100644 --- a/rts/Lua/LuaUnitDefs.cpp +++ b/rts/Lua/LuaUnitDefs.cpp @@ -370,18 +370,18 @@ static int WeaponsTable(lua_State* L, const void* data) lua_pushnumber(L, i + LUA_WEAPON_BASE_INDEX); lua_createtable(L, 0, 10); { - HSTR_PUSH_NUMBER(L, "weaponDef", wd->id); - HSTR_PUSH_NUMBER(L, "slavedTo", udw.slavedTo - 1 + LUA_WEAPON_BASE_INDEX); - HSTR_PUSH_NUMBER(L, "maxAngleDif", udw.maxMainDirAngleDif); - HSTR_PUSH_NUMBER(L, "mainDirX", udw.mainDir.x); - HSTR_PUSH_NUMBER(L, "mainDirY", udw.mainDir.y); - HSTR_PUSH_NUMBER(L, "mainDirZ", udw.mainDir.z); - - HSTR_PUSH(L, "badTargets"); + LuaPushNamedNumber(L, "weaponDef", wd->id); + LuaPushNamedNumber(L, "slavedTo", udw.slavedTo - 1 + LUA_WEAPON_BASE_INDEX); + LuaPushNamedNumber(L, "maxAngleDif", udw.maxMainDirAngleDif); + LuaPushNamedNumber(L, "mainDirX", udw.mainDir.x); + LuaPushNamedNumber(L, "mainDirY", udw.mainDir.y); + LuaPushNamedNumber(L, "mainDirZ", udw.mainDir.z); + + LuaPushString(L, "badTargets"); CategorySetFromBits(L, &udw.badTargetCat); lua_rawset(L, -3); - HSTR_PUSH(L, "onlyTargets"); + LuaPushString(L, "onlyTargets"); CategorySetFromBits(L, &udw.onlyTargetCat); lua_rawset(L, -3); } @@ -404,10 +404,10 @@ static void PushGuiSoundSet(lua_State* L, const string& name, lua_pushnumber(L, i + 1); lua_createtable(L, 0, CLuaHandle::GetHandleSynced(L) ? 2 : 3); const GuiSoundSetData& sound = soundSet.GetSoundData(i); - HSTR_PUSH_STRING(L, "name", sound.name); - HSTR_PUSH_NUMBER(L, "volume", sound.volume); + LuaPushNamedString(L, "name", sound.name); + LuaPushNamedNumber(L, "volume", sound.volume); if (!CLuaHandle::GetHandleSynced(L)) { - HSTR_PUSH_NUMBER(L, "id", sound.id); + LuaPushNamedNumber(L, "id", sound.id); } lua_rawset(L, -3); } @@ -451,22 +451,22 @@ static int MoveDefTable(lua_State* L, const void* data) assert(md->pathType == mdPathType); lua_createtable(L, 0, 14); - HSTR_PUSH_NUMBER(L, "id" , md->pathType); - HSTR_PUSH_NUMBER(L, "smClass" , md->speedModClass); - HSTR_PUSH_NUMBER(L, "xsize" , md->xsize); - HSTR_PUSH_NUMBER(L, "zsize" , md->zsize); - HSTR_PUSH_NUMBER(L, "depth" , md->depth); - HSTR_PUSH_NUMBER(L, "maxSlope" , md->maxSlope); - HSTR_PUSH_NUMBER(L, "slopeMod" , md->slopeMod); - HSTR_PUSH_NUMBER(L, "depthMod" , md->depthModParams[MoveDef::DEPTHMOD_LIN_COEFF]); - HSTR_PUSH_NUMBER(L, "crushStrength", md->crushStrength); - HSTR_PUSH_BOOL (L, "isSubmarine" , md->isSubmarine); - - HSTR_PUSH_BOOL (L, "heatMapping" , md->heatMapping); - HSTR_PUSH_NUMBER(L, "heatMod" , md->heatMod); - HSTR_PUSH_NUMBER(L, "heatProduced" , md->heatProduced); - - HSTR_PUSH_STRING(L, "name" , md->name); + LuaPushNamedNumber(L, "id" , md->pathType); + LuaPushNamedNumber(L, "smClass" , md->speedModClass); + LuaPushNamedNumber(L, "xsize" , md->xsize); + LuaPushNamedNumber(L, "zsize" , md->zsize); + LuaPushNamedNumber(L, "depth" , md->depth); + LuaPushNamedNumber(L, "maxSlope" , md->maxSlope); + LuaPushNamedNumber(L, "slopeMod" , md->slopeMod); + LuaPushNamedNumber(L, "depthMod" , md->depthModParams[MoveDef::DEPTHMOD_LIN_COEFF]); + LuaPushNamedNumber(L, "crushStrength", md->crushStrength); + LuaPushNamedBool (L, "isSubmarine" , md->isSubmarine); + + LuaPushNamedBool (L, "heatMapping" , md->heatMapping); + LuaPushNamedNumber(L, "heatMod" , md->heatMod); + LuaPushNamedNumber(L, "heatProduced" , md->heatProduced); + + LuaPushNamedString(L, "name" , md->name); return 1; } diff --git a/rts/Lua/LuaUnsyncedRead.cpp b/rts/Lua/LuaUnsyncedRead.cpp index 8b4e95a9e0b..28f67f80dfc 100644 --- a/rts/Lua/LuaUnsyncedRead.cpp +++ b/rts/Lua/LuaUnsyncedRead.cpp @@ -3123,7 +3123,7 @@ int LuaUnsyncedRead::GetCameraFOV(lua_State* L) int LuaUnsyncedRead::GetCameraVectors(lua_State* L) { #define PACK_CAMERA_VECTOR(s,n) \ - HSTR_PUSH(L, #s); \ + lua_pushhstring(L, CompileTimeHash(#s), #s, sizeof(#s) - 1); \ lua_createtable(L, 3, 0); \ lua_pushnumber(L, camera-> n .x); lua_rawseti(L, -2, 1); \ lua_pushnumber(L, camera-> n .y); lua_rawseti(L, -2, 2); \ diff --git a/rts/Lua/LuaUtils.cpp b/rts/Lua/LuaUtils.cpp index 6b86bd3e8a1..caebc42cc70 100644 --- a/rts/Lua/LuaUtils.cpp +++ b/rts/Lua/LuaUtils.cpp @@ -762,9 +762,9 @@ int LuaUtils::PushModelRadius(lua_State* L, const SolidObjectDef* def, bool isUn int LuaUtils::PushFeatureModelDrawType(lua_State* L, const FeatureDef* def) { switch (def->drawType) { - case DRAWTYPE_NONE: { HSTR_PUSH(L, "none"); } break; - case DRAWTYPE_MODEL: { HSTR_PUSH(L, "model"); } break; - default: { HSTR_PUSH(L, "tree"); } break; + case DRAWTYPE_NONE: { LuaPushString(L, "none"); } break; + case DRAWTYPE_MODEL: { LuaPushString(L, "model"); } break; + default: { LuaPushString(L, "tree"); } break; } return 1; @@ -805,30 +805,30 @@ int LuaUtils::PushModelTable(lua_State* L, const SolidObjectDef* def) { if (model != nullptr) { // unit, or non-tree feature - HSTR_PUSH_NUMBER(L, "minx", model->mins.x); - HSTR_PUSH_NUMBER(L, "miny", model->mins.y); - HSTR_PUSH_NUMBER(L, "minz", model->mins.z); - HSTR_PUSH_NUMBER(L, "maxx", model->maxs.x); - HSTR_PUSH_NUMBER(L, "maxy", model->maxs.y); - HSTR_PUSH_NUMBER(L, "maxz", model->maxs.z); - - HSTR_PUSH_NUMBER(L, "midx", model->relMidPos.x); - HSTR_PUSH_NUMBER(L, "midy", model->relMidPos.y); - HSTR_PUSH_NUMBER(L, "midz", model->relMidPos.z); + LuaPushNamedNumber(L, "minx", model->mins.x); + LuaPushNamedNumber(L, "miny", model->mins.y); + LuaPushNamedNumber(L, "minz", model->mins.z); + LuaPushNamedNumber(L, "maxx", model->maxs.x); + LuaPushNamedNumber(L, "maxy", model->maxs.y); + LuaPushNamedNumber(L, "maxz", model->maxs.z); + + LuaPushNamedNumber(L, "midx", model->relMidPos.x); + LuaPushNamedNumber(L, "midy", model->relMidPos.y); + LuaPushNamedNumber(L, "midz", model->relMidPos.z); } else { - HSTR_PUSH_NUMBER(L, "minx", 0.0f); - HSTR_PUSH_NUMBER(L, "miny", 0.0f); - HSTR_PUSH_NUMBER(L, "minz", 0.0f); - HSTR_PUSH_NUMBER(L, "maxx", 0.0f); - HSTR_PUSH_NUMBER(L, "maxy", 0.0f); - HSTR_PUSH_NUMBER(L, "maxz", 0.0f); - - HSTR_PUSH_NUMBER(L, "midx", 0.0f); - HSTR_PUSH_NUMBER(L, "midy", 0.0f); - HSTR_PUSH_NUMBER(L, "midz", 0.0f); + LuaPushNamedNumber(L, "minx", 0.0f); + LuaPushNamedNumber(L, "miny", 0.0f); + LuaPushNamedNumber(L, "minz", 0.0f); + LuaPushNamedNumber(L, "maxx", 0.0f); + LuaPushNamedNumber(L, "maxy", 0.0f); + LuaPushNamedNumber(L, "maxz", 0.0f); + + LuaPushNamedNumber(L, "midx", 0.0f); + LuaPushNamedNumber(L, "midy", 0.0f); + LuaPushNamedNumber(L, "midz", 0.0f); } - HSTR_PUSH(L, "textures"); + LuaPushString(L, "textures"); lua_createtable(L, 0, model != nullptr ? 2 : 0); if (model != nullptr) { @@ -850,16 +850,16 @@ int LuaUtils::PushColVolTable(lua_State* L, const CollisionVolume* vol) { lua_createtable(L, 0, 11); switch (vol->GetVolumeType()) { case CollisionVolume::COLVOL_TYPE_ELLIPSOID: - HSTR_PUSH_CSTRING(L, "type", "ellipsoid"); + LuaPushNamedString(L, "type", "ellipsoid"); break; case CollisionVolume::COLVOL_TYPE_CYLINDER: - HSTR_PUSH_CSTRING(L, "type", "cylinder"); + LuaPushNamedString(L, "type", "cylinder"); break; case CollisionVolume::COLVOL_TYPE_BOX: - HSTR_PUSH_CSTRING(L, "type", "box"); + LuaPushNamedString(L, "type", "box"); break; case CollisionVolume::COLVOL_TYPE_SPHERE: - HSTR_PUSH_CSTRING(L, "type", "sphere"); + LuaPushNamedString(L, "type", "sphere"); break; } @@ -917,7 +917,7 @@ int LuaUtils::ParseColVolData(lua_State* L, int idx, CollisionVolume* vol) void LuaUtils::PushCommandParamsTable(lua_State* L, const Command& cmd, bool subtable) { if (subtable) - HSTR_PUSH(L, "params"); + LuaPushString(L, "params"); lua_createtable(L, cmd.GetNumParams(), 0); @@ -950,16 +950,16 @@ void LuaUtils::PushCommandParamsTable(lua_State* L, const Command& cmd, bool sub void LuaUtils::PushCommandOptionsTable(lua_State* L, const Command& cmd, bool subtable) { if (subtable) - HSTR_PUSH(L, "options"); + LuaPushString(L, "options"); lua_createtable(L, 0, 7); - HSTR_PUSH_NUMBER(L, "coded", cmd.GetOpts()); - HSTR_PUSH_BOOL(L, "alt", !!(cmd.GetOpts() & ALT_KEY )); - HSTR_PUSH_BOOL(L, "ctrl", !!(cmd.GetOpts() & CONTROL_KEY )); - HSTR_PUSH_BOOL(L, "shift", !!(cmd.GetOpts() & SHIFT_KEY )); - HSTR_PUSH_BOOL(L, "right", !!(cmd.GetOpts() & RIGHT_MOUSE_KEY)); - HSTR_PUSH_BOOL(L, "meta", !!(cmd.GetOpts() & META_KEY )); - HSTR_PUSH_BOOL(L, "internal", !!(cmd.GetOpts() & INTERNAL_ORDER )); + LuaPushNamedNumber(L, "coded", cmd.GetOpts()); + LuaPushNamedBool(L, "alt", !!(cmd.GetOpts() & ALT_KEY )); + LuaPushNamedBool(L, "ctrl", !!(cmd.GetOpts() & CONTROL_KEY )); + LuaPushNamedBool(L, "shift", !!(cmd.GetOpts() & SHIFT_KEY )); + LuaPushNamedBool(L, "right", !!(cmd.GetOpts() & RIGHT_MOUSE_KEY)); + LuaPushNamedBool(L, "meta", !!(cmd.GetOpts() & META_KEY )); + LuaPushNamedBool(L, "internal", !!(cmd.GetOpts() & INTERNAL_ORDER )); if (subtable) lua_rawset(L, -3); @@ -1598,20 +1598,20 @@ void LuaUtils::PushCommandDesc(lua_State* L, const SCommandDescription& cd) lua_checkstack(L, 1 + 1 + 1 + 1); lua_createtable(L, 0, numTblKeys); - HSTR_PUSH_NUMBER(L, "id", cd.id); - HSTR_PUSH_NUMBER(L, "type", cd.type); - HSTR_PUSH_STRING(L, "name", cd.name); - HSTR_PUSH_STRING(L, "action", cd.action); - HSTR_PUSH_STRING(L, "tooltip", cd.tooltip); - HSTR_PUSH_STRING(L, "texture", cd.iconname); - HSTR_PUSH_STRING(L, "cursor", cd.mouseicon); - HSTR_PUSH_BOOL(L, "queueing", cd.queueing); - HSTR_PUSH_BOOL(L, "hidden", cd.hidden); - HSTR_PUSH_BOOL(L, "disabled", cd.disabled); - HSTR_PUSH_BOOL(L, "showUnique", cd.showUnique); - HSTR_PUSH_BOOL(L, "onlyTexture", cd.onlyTexture); - - HSTR_PUSH(L, "params"); + LuaPushNamedNumber(L, "id", cd.id); + LuaPushNamedNumber(L, "type", cd.type); + LuaPushNamedString(L, "name", cd.name); + LuaPushNamedString(L, "action", cd.action); + LuaPushNamedString(L, "tooltip", cd.tooltip); + LuaPushNamedString(L, "texture", cd.iconname); + LuaPushNamedString(L, "cursor", cd.mouseicon); + LuaPushNamedBool (L, "queueing", cd.queueing); + LuaPushNamedBool (L, "hidden", cd.hidden); + LuaPushNamedBool (L, "disabled", cd.disabled); + LuaPushNamedBool (L, "showUnique", cd.showUnique); + LuaPushNamedBool (L, "onlyTexture", cd.onlyTexture); + + LuaPushString(L, "params"); lua_createtable(L, 0, numParams); diff --git a/rts/Lua/LuaUtils.h b/rts/Lua/LuaUtils.h index 1ff85177a1c..4c40fcb44d1 100644 --- a/rts/Lua/LuaUtils.h +++ b/rts/Lua/LuaUtils.h @@ -12,6 +12,7 @@ #include "LuaInclude.h" #include "LuaHandle.h" #include "LuaDefs.h" +#include "System/StringHash.h" // FIXME: use fwd-decls #include "System/EventClient.h" #include "Sim/Units/CommandAI/Command.h" @@ -247,7 +248,18 @@ static void PushObjectDefProxyTable( lua_rawset(L, -3); // set the proxy table } +// Note: The `const char (&key) [N]` overloads are a specialization for string literals to allow hashing at compile time. +// For a `char buf[N]` the hasher will assume its a C style string of size N terminated at index N-1. +// This can lead to hashing after the null terminator if the string only uses part of the buffer. +// Use a std::string if you want to pass in dynamic keys. +template +static inline void LuaPushNamedNil(lua_State* L, const char (&key) [N]) +{ + lua_pushhstring(L, CompileTimeHash(key), key, N - 1); + lua_pushnil(L); + lua_rawset(L, -3); +} static inline void LuaPushNamedNil(lua_State* L, const string& key) @@ -257,6 +269,13 @@ static inline void LuaPushNamedNil(lua_State* L, lua_rawset(L, -3); } +template +static inline void LuaPushNamedBool(lua_State* L, const char (&key) [N], bool value) +{ + lua_pushhstring(L, CompileTimeHash(key), key, N - 1); + lua_pushboolean(L, value); + lua_rawset(L, -3); +} static inline void LuaPushNamedBool(lua_State* L, const string& key, bool value) { @@ -265,6 +284,13 @@ static inline void LuaPushNamedBool(lua_State* L, const string& key, bool value) lua_rawset(L, -3); } +template +static inline void LuaPushNamedNumber(lua_State* L, const char (&key) [N], lua_Number value) +{ + lua_pushhstring(L, CompileTimeHash(key), key, N - 1); + lua_pushnumber(L, value); + lua_rawset(L, -3); +} static inline void LuaPushNamedNumber(lua_State* L, const string& key, lua_Number value) { @@ -273,6 +299,13 @@ static inline void LuaPushNamedNumber(lua_State* L, const string& key, lua_Numbe lua_rawset(L, -3); } +template +static inline void LuaPushNamedChar(lua_State* L, const char (&key) [N], char value) +{ + lua_pushhstring(L, CompileTimeHash(key), key, N - 1); + lua_pushlstring(L, &value, 1); + lua_rawset(L, -3); +} static inline void LuaPushNamedChar(lua_State* L, char const *name, char value) { @@ -281,6 +314,22 @@ static inline void LuaPushNamedChar(lua_State* L, char const *name, char value) lua_rawset(L, -3); } +template +static inline void LuaPushNamedString(lua_State* L, const char (&key) [N0], const char (&value) [N1]) +{ + lua_pushhstring(L, CompileTimeHash(key), key, N0 - 1); + lua_pushhstring(L, CompileTimeHash(value), value, N1 - 1); + lua_rawset(L, -3); +} + +template +static inline void LuaPushNamedString(lua_State* L, const char (&key) [N], const string& value) +{ + lua_pushhstring(L, CompileTimeHash(key), key, N - 1); + lua_pushsstring(L, value); + lua_rawset(L, -3); +} + static inline void LuaPushNamedString(lua_State* L, const string& key, const string& value) { lua_pushsstring(L, key); @@ -288,6 +337,13 @@ static inline void LuaPushNamedString(lua_State* L, const string& key, const str lua_rawset(L, -3); } +template +static inline void LuaPushNamedCFunc(lua_State* L, const char (&key) [N], lua_CFunction func) +{ + lua_pushhstring(L, CompileTimeHash(key), key, N - 1); + lua_pushcfunction(L, func); + lua_rawset(L, -3); +} static inline void LuaPushNamedCFunc(lua_State* L, const string& key, lua_CFunction func) { @@ -303,6 +359,17 @@ static inline void LuaPushRawNamedCFunc(lua_State* L, const char* key, lua_CFunc lua_rawset(L, -3); } +template +static inline void LuaPushString(lua_State* L, const char (&str)[N]) +{ + static_assert(N > 1); + lua_pushhstring(L, CompileTimeHash(str), str, N - 1); +} + +static inline void LuaPushString(lua_State* L, const string& str) { + lua_pushsstring(L, str); +} + #define REGISTER_LUA_CFUNC(func) LuaPushRawNamedCFunc(L, #func, func) #define REGISTER_NAMED_LUA_CFUNC(name, func) LuaPushRawNamedCFunc(L, name, func) #define REGISTER_SCOPED_LUA_CFUNC(scope, func) LuaPushRawNamedCFunc(L, #func, scope::func) diff --git a/rts/Lua/LuaVFS.cpp b/rts/Lua/LuaVFS.cpp index 1c2bab2796b..d0f94c7e534 100644 --- a/rts/Lua/LuaVFS.cpp +++ b/rts/Lua/LuaVFS.cpp @@ -149,57 +149,57 @@ bool LuaVFS::PushCommon(lua_State* L) { /*** @field VFS.RAW "r" Only select uncompressed files. */ - HSTR_PUSH_CSTRING(L, "RAW", SPRING_VFS_RAW); + LuaPushNamedString(L, "RAW", SPRING_VFS_RAW); /*** @field VFS.GAME "M" */ - HSTR_PUSH_CSTRING(L, "GAME", SPRING_VFS_MOD); // synonym to MOD + LuaPushNamedString(L, "GAME", SPRING_VFS_MOD); // synonym to MOD /*** @field VFS.MAP "m" */ - HSTR_PUSH_CSTRING(L, "MAP", SPRING_VFS_MAP); + LuaPushNamedString(L, "MAP", SPRING_VFS_MAP); /*** @field VFS.BASE "b" */ - HSTR_PUSH_CSTRING(L, "BASE", SPRING_VFS_BASE); + LuaPushNamedString(L, "BASE", SPRING_VFS_BASE); /*** @field VFS.MENU "e" */ - HSTR_PUSH_CSTRING(L, "MENU", SPRING_VFS_MENU); + LuaPushNamedString(L, "MENU", SPRING_VFS_MENU); /*** @field VFS.ZIP "Mmeb" Only select compressed files (`.sdz`, `.sd7`). */ - HSTR_PUSH_CSTRING(L, "ZIP", SPRING_VFS_ZIP); + LuaPushNamedString(L, "ZIP", SPRING_VFS_ZIP); /*** @field VFS.RAW_FIRST "rMmeb" Try uncompressed files first, then compressed. */ - HSTR_PUSH_CSTRING(L, "RAW_FIRST", SPRING_VFS_RAW_FIRST); + LuaPushNamedString(L, "RAW_FIRST", SPRING_VFS_RAW_FIRST); /*** @field VFS.ZIP_FIRST "Mmebr" Try compressed files first, then uncompressed. */ - HSTR_PUSH_CSTRING(L, "ZIP_FIRST", SPRING_VFS_ZIP_FIRST); + LuaPushNamedString(L, "ZIP_FIRST", SPRING_VFS_ZIP_FIRST); /*** * @deprecated * @field VFS.MOD "M" Older spelling for `VFS.GAME` */ - HSTR_PUSH_CSTRING(L, "MOD", SPRING_VFS_MOD); + LuaPushNamedString(L, "MOD", SPRING_VFS_MOD); /*** * @deprecated * @field VFS.RAW_ONLY "r" */ - HSTR_PUSH_CSTRING(L, "RAW_ONLY", SPRING_VFS_RAW); // backwards compatibility + LuaPushNamedString(L, "RAW_ONLY", SPRING_VFS_RAW); // backwards compatibility /*** * @deprecated * @field VFS.ZIP_ONLY "Mmeb" */ - HSTR_PUSH_CSTRING(L, "ZIP_ONLY", SPRING_VFS_ZIP); // backwards compatibility - - HSTR_PUSH_CFUNC(L, "PackU8", PackU8); - HSTR_PUSH_CFUNC(L, "PackU16", PackU16); - HSTR_PUSH_CFUNC(L, "PackU32", PackU32); - HSTR_PUSH_CFUNC(L, "PackS8", PackS8); - HSTR_PUSH_CFUNC(L, "PackS16", PackS16); - HSTR_PUSH_CFUNC(L, "PackS32", PackS32); - HSTR_PUSH_CFUNC(L, "PackF32", PackF32); - HSTR_PUSH_CFUNC(L, "UnpackU8", UnpackU8); - HSTR_PUSH_CFUNC(L, "UnpackU16", UnpackU16); - HSTR_PUSH_CFUNC(L, "UnpackU32", UnpackU32); - HSTR_PUSH_CFUNC(L, "UnpackS8", UnpackS8); - HSTR_PUSH_CFUNC(L, "UnpackS16", UnpackS16); - HSTR_PUSH_CFUNC(L, "UnpackS32", UnpackS32); - HSTR_PUSH_CFUNC(L, "UnpackF32", UnpackF32); + LuaPushNamedString(L, "ZIP_ONLY", SPRING_VFS_ZIP); // backwards compatibility + + LuaPushNamedCFunc(L, "PackU8", PackU8); + LuaPushNamedCFunc(L, "PackU16", PackU16); + LuaPushNamedCFunc(L, "PackU32", PackU32); + LuaPushNamedCFunc(L, "PackS8", PackS8); + LuaPushNamedCFunc(L, "PackS16", PackS16); + LuaPushNamedCFunc(L, "PackS32", PackS32); + LuaPushNamedCFunc(L, "PackF32", PackF32); + LuaPushNamedCFunc(L, "UnpackU8", UnpackU8); + LuaPushNamedCFunc(L, "UnpackU16", UnpackU16); + LuaPushNamedCFunc(L, "UnpackU32", UnpackU32); + LuaPushNamedCFunc(L, "UnpackS8", UnpackS8); + LuaPushNamedCFunc(L, "UnpackS16", UnpackS16); + LuaPushNamedCFunc(L, "UnpackS32", UnpackS32); + LuaPushNamedCFunc(L, "UnpackF32", UnpackF32); // compression should be safe in synced context - HSTR_PUSH_CFUNC(L, "ZlibCompress", ZlibCompress); - HSTR_PUSH_CFUNC(L, "ZlibDecompress", ZlibDecompress); - HSTR_PUSH_CFUNC(L, "CalculateHash", CalculateHash); + LuaPushNamedCFunc(L, "ZlibCompress", ZlibCompress); + LuaPushNamedCFunc(L, "ZlibDecompress", ZlibDecompress); + LuaPushNamedCFunc(L, "CalculateHash", CalculateHash); return true; } @@ -209,11 +209,11 @@ bool LuaVFS::PushSynced(lua_State* L) { PushCommon(L); - HSTR_PUSH_CFUNC(L, "Include", SyncInclude); - HSTR_PUSH_CFUNC(L, "LoadFile", SyncLoadFile); - HSTR_PUSH_CFUNC(L, "FileExists", SyncFileExists); - HSTR_PUSH_CFUNC(L, "DirList", SyncDirList); - HSTR_PUSH_CFUNC(L, "SubDirs", SyncSubDirs); + LuaPushNamedCFunc(L, "Include", SyncInclude); + LuaPushNamedCFunc(L, "LoadFile", SyncLoadFile); + LuaPushNamedCFunc(L, "FileExists", SyncFileExists); + LuaPushNamedCFunc(L, "DirList", SyncDirList); + LuaPushNamedCFunc(L, "SubDirs", SyncSubDirs); return true; } @@ -223,21 +223,21 @@ bool LuaVFS::PushUnsynced(lua_State* L) { PushCommon(L); - HSTR_PUSH_CFUNC(L, "Include", UnsyncInclude); - HSTR_PUSH_CFUNC(L, "LoadFile", UnsyncLoadFile); - HSTR_PUSH_CFUNC(L, "FileExists", UnsyncFileExists); - HSTR_PUSH_CFUNC(L, "DirList", UnsyncDirList); - HSTR_PUSH_CFUNC(L, "SubDirs", UnsyncSubDirs); + LuaPushNamedCFunc(L, "Include", UnsyncInclude); + LuaPushNamedCFunc(L, "LoadFile", UnsyncLoadFile); + LuaPushNamedCFunc(L, "FileExists", UnsyncFileExists); + LuaPushNamedCFunc(L, "DirList", UnsyncDirList); + LuaPushNamedCFunc(L, "SubDirs", UnsyncSubDirs); - HSTR_PUSH_CFUNC(L, "GetFileAbsolutePath", GetFileAbsolutePath); - HSTR_PUSH_CFUNC(L, "GetArchiveContainingFile", GetArchiveContainingFile); + LuaPushNamedCFunc(L, "GetFileAbsolutePath", GetFileAbsolutePath); + LuaPushNamedCFunc(L, "GetArchiveContainingFile", GetArchiveContainingFile); - HSTR_PUSH_CFUNC(L, "UseArchive", UseArchive); - HSTR_PUSH_CFUNC(L, "CompressFolder", CompressFolder); + LuaPushNamedCFunc(L, "UseArchive", UseArchive); + LuaPushNamedCFunc(L, "CompressFolder", CompressFolder); // Removed due to sync unsafety, see commit 0ee88788931f9f0b195eb5f895f1092fde4211c0 - // HSTR_PUSH_CFUNC(L, "MapArchive", MapArchive); - // HSTR_PUSH_CFUNC(L, "UnmapArchive", UnmapArchive); + // LuaPushNamedCFunc(L, "MapArchive", MapArchive); + // LuaPushNamedCFunc(L, "UnmapArchive", UnmapArchive); return true; } diff --git a/rts/Lua/LuaWeaponDefs.cpp b/rts/Lua/LuaWeaponDefs.cpp index 885feba6a1e..b2815d8cf77 100644 --- a/rts/Lua/LuaWeaponDefs.cpp +++ b/rts/Lua/LuaWeaponDefs.cpp @@ -245,11 +245,11 @@ static int DamagesArray(lua_State* L, const void* data) const DamageArray& d = *static_cast(data); lua_createtable(L, damageArrayHandler.GetNumTypes(), 5); - HSTR_PUSH_NUMBER(L, "impulseFactor", d.impulseFactor); - HSTR_PUSH_NUMBER(L, "impulseBoost", d.impulseBoost); - HSTR_PUSH_NUMBER(L, "craterMult", d.craterMult); - HSTR_PUSH_NUMBER(L, "craterBoost", d.craterBoost); - HSTR_PUSH_NUMBER(L, "paralyzeDamageTime", d.paralyzeDamageTime); + LuaPushNamedNumber(L, "impulseFactor", d.impulseFactor); + LuaPushNamedNumber(L, "impulseBoost", d.impulseBoost); + LuaPushNamedNumber(L, "craterMult", d.craterMult); + LuaPushNamedNumber(L, "craterBoost", d.craterBoost); + LuaPushNamedNumber(L, "paralyzeDamageTime", d.paralyzeDamageTime); // damage values for (int i = 0, n = damageArrayHandler.GetNumTypes(); i < n; i++) { @@ -266,34 +266,34 @@ static int VisualsTable(lua_State* L, const void* data) { const struct WeaponDef::Visuals& v = *static_cast(data); lua_createtable(L, 0, 28); - HSTR_PUSH_STRING(L, "modelName", modelLoader.FindModelPath(v.modelName)); - HSTR_PUSH_NUMBER(L, "colorR", v.color.x); - HSTR_PUSH_NUMBER(L, "colorG", v.color.y); - HSTR_PUSH_NUMBER(L, "colorB", v.color.z); - HSTR_PUSH_NUMBER(L, "color2R", v.color2.x); - HSTR_PUSH_NUMBER(L, "color2G", v.color2.y); - HSTR_PUSH_NUMBER(L, "color2B", v.color2.z); - HSTR_PUSH_BOOL (L, "smokeTrail", v.smokeTrail); - HSTR_PUSH_BOOL (L, "smokeTrailCastShadow", v.smokeTrailCastShadow); - HSTR_PUSH_NUMBER(L, "smokePeriod", v.smokePeriod); - HSTR_PUSH_NUMBER(L, "smokeTime", v.smokeTime); - HSTR_PUSH_NUMBER(L, "smokeSize", v.smokeSize); - HSTR_PUSH_NUMBER(L, "smokeColor", v.smokeColor); - HSTR_PUSH_NUMBER(L, "tileLength", v.tilelength); - HSTR_PUSH_NUMBER(L, "scrollSpeed", v.scrollspeed); - HSTR_PUSH_NUMBER(L, "pulseSpeed", v.pulseSpeed); - HSTR_PUSH_NUMBER(L, "laserFlareSize", v.laserflaresize); - HSTR_PUSH_NUMBER(L, "thickness", v.thickness); - HSTR_PUSH_NUMBER(L, "coreThickness", v.corethickness); - HSTR_PUSH_NUMBER(L, "beamDecay", v.beamdecay); - HSTR_PUSH_NUMBER(L, "stages", v.stages); - HSTR_PUSH_NUMBER(L, "sizeDecay", v.sizeDecay); - HSTR_PUSH_NUMBER(L, "alphaDecay", v.alphaDecay); - HSTR_PUSH_NUMBER(L, "separation", v.separation); - HSTR_PUSH_BOOL (L, "castShadow", v.castShadow); - HSTR_PUSH_BOOL (L, "noGap", v.noGap); - HSTR_PUSH_BOOL (L, "alwaysVisible", v.alwaysVisible); - HSTR_PUSH_BOOL (L, "beamWeapon", false); // DEPRECATED + LuaPushNamedString(L, "modelName", modelLoader.FindModelPath(v.modelName)); + LuaPushNamedNumber(L, "colorR", v.color.x); + LuaPushNamedNumber(L, "colorG", v.color.y); + LuaPushNamedNumber(L, "colorB", v.color.z); + LuaPushNamedNumber(L, "color2R", v.color2.x); + LuaPushNamedNumber(L, "color2G", v.color2.y); + LuaPushNamedNumber(L, "color2B", v.color2.z); + LuaPushNamedBool (L, "smokeTrail", v.smokeTrail); + LuaPushNamedBool (L, "smokeTrailCastShadow", v.smokeTrailCastShadow); + LuaPushNamedNumber(L, "smokePeriod", v.smokePeriod); + LuaPushNamedNumber(L, "smokeTime", v.smokeTime); + LuaPushNamedNumber(L, "smokeSize", v.smokeSize); + LuaPushNamedNumber(L, "smokeColor", v.smokeColor); + LuaPushNamedNumber(L, "tileLength", v.tilelength); + LuaPushNamedNumber(L, "scrollSpeed", v.scrollspeed); + LuaPushNamedNumber(L, "pulseSpeed", v.pulseSpeed); + LuaPushNamedNumber(L, "laserFlareSize", v.laserflaresize); + LuaPushNamedNumber(L, "thickness", v.thickness); + LuaPushNamedNumber(L, "coreThickness", v.corethickness); + LuaPushNamedNumber(L, "beamDecay", v.beamdecay); + LuaPushNamedNumber(L, "stages", v.stages); + LuaPushNamedNumber(L, "sizeDecay", v.sizeDecay); + LuaPushNamedNumber(L, "alphaDecay", v.alphaDecay); + LuaPushNamedNumber(L, "separation", v.separation); + LuaPushNamedBool (L, "castShadow", v.castShadow); + LuaPushNamedBool (L, "noGap", v.noGap); + LuaPushNamedBool (L, "alwaysVisible", v.alwaysVisible); + LuaPushNamedBool (L, "beamWeapon", false); // DEPRECATED return 1; } @@ -409,11 +409,11 @@ static int GuiSoundSetTable(lua_State* L, const void* data) const GuiSoundSetData& sound = soundSet.GetSoundData(i); - HSTR_PUSH_STRING(L, "name", sound.name); - HSTR_PUSH_NUMBER(L, "volume", sound.volume); + LuaPushNamedString(L, "name", sound.name); + LuaPushNamedNumber(L, "volume", sound.volume); if (!CLuaHandle::GetHandleSynced(L)) { - HSTR_PUSH_NUMBER(L, "id", sound.id); + LuaPushNamedNumber(L, "id", sound.id); } lua_rawset(L, -3); diff --git a/rts/Lua/LuaZip.cpp b/rts/Lua/LuaZip.cpp index daf209b7d67..ba0dc62eee3 100644 --- a/rts/Lua/LuaZip.cpp +++ b/rts/Lua/LuaZip.cpp @@ -36,6 +36,7 @@ #include "LuaZip.h" #include "LuaInclude.h" #include "LuaHashString.h" +#include "LuaUtils.h" #include "System/FileSystem/Archives/IArchive.h" #include "System/FileSystem/ArchiveLoader.h" #include "System/FileSystem/DataDirsAccess.h" @@ -79,7 +80,7 @@ bool LuaZipFileWriter::PushUnsynced(lua_State* L) CreateMetatable(L); // FIXME when this is enabled LuaGaia/LuaRules unsynced has access to it too! - //HSTR_PUSH_CFUNC(L, "CreateZipFileWriter", open); + // LuaPushNamedCFunc(L, "CreateZipFileWriter", open); return true; } @@ -93,10 +94,10 @@ bool LuaZipFileWriter::CreateMetatable(lua_State* L) lua_pushvalue(L, -1); lua_setfield(L, -2, "__index"); - HSTR_PUSH_CFUNC(L, "__gc", meta_gc); - HSTR_PUSH_CFUNC(L, "close", meta_gc); - HSTR_PUSH_CFUNC(L, "open", meta_open); - HSTR_PUSH_CFUNC(L, "write", meta_write); + LuaPushNamedCFunc(L, "__gc", meta_gc); + LuaPushNamedCFunc(L, "close", meta_gc); + LuaPushNamedCFunc(L, "open", meta_open); + LuaPushNamedCFunc(L, "write", meta_write); lua_pop(L, 1); return true; @@ -238,7 +239,7 @@ bool LuaZipFileReader::PushUnsynced(lua_State* L) CreateMetatable(L); // FIXME when this is enabled LuaGaia/LuaRules unsynced has access to it too! - //HSTR_PUSH_CFUNC(L, "CreateZipFileReader", open); + // LuaPushNamedCFunc(L, "CreateZipFileReader", open); return true; } @@ -252,10 +253,10 @@ bool LuaZipFileReader::CreateMetatable(lua_State* L) lua_pushvalue(L, -1); lua_setfield(L, -2, "__index"); - HSTR_PUSH_CFUNC(L, "__gc", meta_gc); - HSTR_PUSH_CFUNC(L, "close", meta_gc); - HSTR_PUSH_CFUNC(L, "open", meta_open); - HSTR_PUSH_CFUNC(L, "read", meta_read); + LuaPushNamedCFunc(L, "__gc", meta_gc); + LuaPushNamedCFunc(L, "close", meta_gc); + LuaPushNamedCFunc(L, "open", meta_open); + LuaPushNamedCFunc(L, "read", meta_read); lua_pop(L, 1); return true; diff --git a/rts/System/StringHash.h b/rts/System/StringHash.h index 2a7fede1a22..98218fefd1a 100644 --- a/rts/System/StringHash.h +++ b/rts/System/StringHash.h @@ -36,4 +36,9 @@ struct compileTimeHasher { } }; -#define COMPILE_TIME_HASH(str) compileTimeHasher::hash(str) +template +[[nodiscard]] constexpr uint32_t CompileTimeHash(const char (&literal)[N]) noexcept +{ + static_assert(N > 1); + return compileTimeHasher::hash(literal); +} \ No newline at end of file From 946aabe03ae7cc01d43af4b3aac83f941a5b7c77 Mon Sep 17 00:00:00 2001 From: PtaQQ Date: Thu, 23 Jul 2026 02:39:34 +0200 Subject: [PATCH 60/90] Debug logs for loading splat normals --- rts/Map/SMF/SMFReadMap.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/rts/Map/SMF/SMFReadMap.cpp b/rts/Map/SMF/SMFReadMap.cpp index 4b007810a9c..dc70d599097 100644 --- a/rts/Map/SMF/SMFReadMap.cpp +++ b/rts/Map/SMF/SMFReadMap.cpp @@ -286,6 +286,8 @@ void CSMFReadMap::CreateSplatDetailTextures() if (!haveSplatNormalDistribTexture) return; + uint32_t loadedSplatNormals = 0; + for (size_t i = 0; i < mapInfo->smf.splatDetailNormalTexNames.size(); i++) { if (i == NUM_SPLAT_DETAIL_NORMALS) break; @@ -302,8 +304,11 @@ void CSMFReadMap::CreateSplatDetailTextures() splatNormalTextures[i].SetRawTexID(splatDetailNormalTextureBM.CreateMipMapTexture(texAnisotropyLevels[true], 0.0f, 0)); splatNormalTextures[i].SetRawSize(int2(splatDetailNormalTextureBM.xsize, splatDetailNormalTextureBM.ysize)); + loadedSplatNormals += (splatNormalTextures[i].GetID() != 0); } + LOG_L(L_DEBUG, "[CSMFReadMap::%s] Loaded %u DNTS splat normal textures", __func__, loadedSplatNormals); + } From 510b1aea40678f31f59932a77cd569bd987e2f49 Mon Sep 17 00:00:00 2001 From: PtaQQ Date: Thu, 23 Jul 2026 02:41:49 +0200 Subject: [PATCH 61/90] Fix SMF DNTS gating and fallback textures --- rts/Map/SMF/SMFReadMap.cpp | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/rts/Map/SMF/SMFReadMap.cpp b/rts/Map/SMF/SMFReadMap.cpp index dc70d599097..436e7e27944 100644 --- a/rts/Map/SMF/SMFReadMap.cpp +++ b/rts/Map/SMF/SMFReadMap.cpp @@ -78,9 +78,6 @@ CSMFReadMap::CSMFReadMap(const std::string& mapName): CEventClient("[CSMFReadMap haveSplatNormalDistribTexture |= !texName.empty(); } - // Detail Normal Splatting requires at least one splatDetailNormalTexture and a distribution texture - haveSplatNormalDistribTexture &= !mapInfo->smf.splatDistrTexName.empty(); - ParseHeader(); LoadHeightMap(); CReadMap::Initialize(); @@ -252,17 +249,27 @@ void CSMFReadMap::CreateSpecularTex() void CSMFReadMap::CreateSplatDetailTextures() { RECOIL_DETAILED_TRACY_ZONE; - if (!haveSplatDetailDistribTexture) + if (!haveSplatDetailDistribTexture && !haveSplatNormalDistribTexture) return; + if (haveSplatNormalDistribTexture && !haveSplatDetailDistribTexture) { + LOG_L(L_DEBUG, "[CSMFReadMap::%s] DNTS active without complete classic splat pair; using fallback splatDetailTex/splatDistrTex", __func__); + } + { CBitmap splatDetailTexBM; + const bool haveSplatDetailTexName = !mapInfo->smf.splatDetailTexName.empty(); // if a map supplies an intensity- AND a distribution-texture for // detail-splat blending, the regular detail-texture is not used // default detail-texture should be all-grey - if (!splatDetailTexBM.Load(mapInfo->smf.splatDetailTexName)) { - LOG_L(L_WARNING, "[CSMFReadMap::%s] Invalid SMF splatDetailTex %s. Creating fallback texture", __func__, mapInfo->smf.splatDetailTexName.c_str()); + if (!haveSplatDetailTexName || !splatDetailTexBM.Load(mapInfo->smf.splatDetailTexName)) { + if (haveSplatDetailTexName) { + LOG_L(L_WARNING, "[CSMFReadMap::%s] Invalid SMF splatDetailTex %s. Creating fallback texture", __func__, mapInfo->smf.splatDetailTexName.c_str()); + } else { + LOG_L(L_DEBUG, "[CSMFReadMap::%s] Missing SMF splatDetailTex. Creating fallback texture", __func__); + } + splatDetailTexBM.AllocDummy(SColor(127, 127, 127, 127)); } @@ -272,9 +279,15 @@ void CSMFReadMap::CreateSplatDetailTextures() { CBitmap splatDistrTexBM; + const bool haveSplatDistrTexName = !mapInfo->smf.splatDistrTexName.empty(); + + if (!haveSplatDistrTexName || !splatDistrTexBM.Load(mapInfo->smf.splatDistrTexName)) { + if (haveSplatDistrTexName) { + LOG_L(L_WARNING, "[CSMFReadMap::%s] Invalid SMF splatDistrTex %s. Creating fallback texture", __func__, mapInfo->smf.splatDistrTexName.c_str()); + } else { + LOG_L(L_DEBUG, "[CSMFReadMap::%s] Missing SMF splatDistrTex. Creating fallback texture", __func__); + } - if (!splatDistrTexBM.Load(mapInfo->smf.splatDistrTexName)) { - LOG_L(L_WARNING, "[CSMFReadMap::%s] Invalid SMF splatDistrTex %s. Creating fallback texture", __func__, mapInfo->smf.splatDistrTexName.c_str()); splatDistrTexBM.AllocDummy(SColor(255, 0, 0, 0)); } From bcd9533ad9741d9a9dc139f1380d9461fcb74380 Mon Sep 17 00:00:00 2001 From: PtaQQ Date: Thu, 23 Jul 2026 02:46:56 +0200 Subject: [PATCH 62/90] Add mapoptions for blank map splats * blank_map_splatdetailtex * blank_map_splatdistr * blank_map_splattexscale1 .. 4 * blank_map_splattexmult1 .. 4 * blank_map_splatdetailnormaltex1 .. 4 * blank_map_splatdetailnormaldiffusealpha --- .../mapgenerator/mapinfo_template.lua | 13 +++-- rts/Map/Generation/BlankMapGenerator.cpp | 55 +++++++++++++++++++ 2 files changed, 64 insertions(+), 4 deletions(-) diff --git a/cont/base/springcontent/mapgenerator/mapinfo_template.lua b/cont/base/springcontent/mapgenerator/mapinfo_template.lua index 67afee82538..eda662b6833 100644 --- a/cont/base/springcontent/mapgenerator/mapinfo_template.lua +++ b/cont/base/springcontent/mapgenerator/mapinfo_template.lua @@ -100,16 +100,21 @@ local mapinfo = { --grassShadingTex = "", --detailTex = "", --specularTex = "", - --splatDetailTex = "", - --splatDistrTex = "", + splatDetailTex = ${SPLAT_DETAIL_TEX}, + splatDistrTex = ${SPLAT_DISTR_TEX}, + splatDetailNormalTex1 = ${SPLAT_DETAIL_NORMAL_TEX_1}, + splatDetailNormalTex2 = ${SPLAT_DETAIL_NORMAL_TEX_2}, + splatDetailNormalTex3 = ${SPLAT_DETAIL_NORMAL_TEX_3}, + splatDetailNormalTex4 = ${SPLAT_DETAIL_NORMAL_TEX_4}, + splatDetailNormalDiffuseAlpha = ${SPLAT_DETAIL_NORMAL_DIFFUSE_ALPHA}, --skyReflectModTex = "", --detailNormalTex = "", --lightEmissionTex = "", }, splats = { - texScales = {0.02, 0.02, 0.02, 0.02}, - texMults = {1.0, 1.0, 1.0, 1.0}, + texScales = {${SPLAT_TEXSCALES}}, + texMults = {${SPLAT_TEXMULTS}}, }, atmosphere = { diff --git a/rts/Map/Generation/BlankMapGenerator.cpp b/rts/Map/Generation/BlankMapGenerator.cpp index d86fca00466..88c0f0c07ac 100644 --- a/rts/Map/Generation/BlankMapGenerator.cpp +++ b/rts/Map/Generation/BlankMapGenerator.cpp @@ -11,6 +11,8 @@ #include "System/FileSystem/VFSHandler.h" #include "System/Log/ILog.h" +#include "fmt/ranges.h" + #include "lib/squish/squish.h" #include @@ -20,6 +22,27 @@ #include // strcpy,memset #include +namespace { + +std::string EscapeLuaString(const std::string& input) +{ + std::string escaped; + escaped.reserve(input.size() + 2); + + escaped += '\"'; + for (char c: input) { + if (c == '\\' || c == '"') + escaped += '\\'; + + escaped += c; + } + escaped += '\"'; + + return escaped; +} + +} // namespace + #include "System/Misc/TracyDefs.h" CBlankMapGenerator::CBlankMapGenerator(const CGameSetup* setup) @@ -211,6 +234,13 @@ void CBlankMapGenerator::GenerateSMF(CVirtualFile* fileSMF) void CBlankMapGenerator::GenerateMapInfo(CVirtualFile* fileMapInfo) { RECOIL_DETAILED_TRACY_ZONE; + const auto& mapOpts = setup->GetMapOptionsCont(); + auto GetBlankMapOpt = [&](const std::string& key) -> std::string { + const std::string blankMapKey = "blank_map_" + key; + const std::string *const optValue = Recoil::map_try_get(mapOpts, blankMapKey); + return optValue ? *optValue : ""; + }; + //Open template mapinfo.lua const std::string luaTemplate = "mapgenerator/mapinfo_template.lua"; CFileHandler fh(luaTemplate, SPRING_VFS_PWD_ALL); @@ -228,11 +258,36 @@ void CBlankMapGenerator::GenerateMapInfo(CVirtualFile* fileMapInfo) } startPosString = ss.str(); + std::array splatTexScaleValues = {{"0.02", "0.02", "0.02", "0.02"}}; + std::array splatTexMultValues = {{"1.0" , "1.0" , "1.0" , "1.0" }}; + for (int i = 0; i < 4; ++i) { + if (const std::string texScale = GetBlankMapOpt(IntToString(i + 1, "splattexscale%i")); !texScale.empty()) + splatTexScaleValues[i] = texScale; + if (const std::string texMult = GetBlankMapOpt(IntToString(i + 1, "splattexmult%i")); !texMult.empty()) + splatTexMultValues [i] = texMult; + } + + bool splatDetailNormalDiffuseAlpha = StringToBool(GetBlankMapOpt("splatdetailnormaldiffusealpha")); + + const auto StringOrNil = [] (const std::string &str) { + return str.empty() ? "nil" : EscapeLuaString(str); + }; + //Replace tags in mapinfo.lua luaInfo = StringReplace(luaInfo, "${NAME}", setup->mapName); luaInfo = StringReplace(luaInfo, "${DESCRIPTION}", mapDescription); luaInfo = StringReplace(luaInfo, "${START_POSITIONS}", startPosString); + luaInfo = StringReplace(luaInfo, "${SPLAT_TEXSCALES}", fmt::format("{}", fmt::join(splatTexScaleValues, ", "))); + luaInfo = StringReplace(luaInfo, "${SPLAT_TEXMULTS}", fmt::format("{}", fmt::join(splatTexMultValues, ", "))); + luaInfo = StringReplace(luaInfo, "${SPLAT_DETAIL_TEX}", StringOrNil(GetBlankMapOpt("splatdetailtex"))); + luaInfo = StringReplace(luaInfo, "${SPLAT_DISTR_TEX}", StringOrNil(GetBlankMapOpt("splatdistr"))); + luaInfo = StringReplace(luaInfo, "${SPLAT_DETAIL_NORMAL_TEX_1}", StringOrNil(GetBlankMapOpt("splatdetailnormaltex1"))); + luaInfo = StringReplace(luaInfo, "${SPLAT_DETAIL_NORMAL_TEX_2}", StringOrNil(GetBlankMapOpt("splatdetailnormaltex2"))); + luaInfo = StringReplace(luaInfo, "${SPLAT_DETAIL_NORMAL_TEX_3}", StringOrNil(GetBlankMapOpt("splatdetailnormaltex3"))); + luaInfo = StringReplace(luaInfo, "${SPLAT_DETAIL_NORMAL_TEX_4}", StringOrNil(GetBlankMapOpt("splatdetailnormaltex4"))); + luaInfo = StringReplace(luaInfo, "${SPLAT_DETAIL_NORMAL_DIFFUSE_ALPHA}", splatDetailNormalDiffuseAlpha ? "true" : "false"); + //Copy to filebuffer fileMapInfo->buffer.assign(luaInfo.begin(), luaInfo.end()); } From 1f5323a2c9294d84655f407c1f52fb79708844a2 Mon Sep 17 00:00:00 2001 From: bruno-dasilva <8520801+bruno-dasilva@users.noreply.github.com> Date: Sat, 25 Jul 2026 23:32:48 -0700 Subject: [PATCH 63/90] fix(synctest): workaround widget timing nondeterminism (#3124) disable luaui, and pass an explicit start frame to the synctest command so the run anchors to a fixed sim frame instead of whenever the message reaches synced code. --- .github/workflows/synctest.yml | 2 +- test/synctest/README.md | 8 +++++++- test/synctest/synctest-startscript.txt | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/synctest.yml b/.github/workflows/synctest.yml index c01baeaa60a..cee0fe4e3b3 100644 --- a/.github/workflows/synctest.yml +++ b/.github/workflows/synctest.yml @@ -33,7 +33,7 @@ defaults: env: MAP: "Jade Empress 1.41" - GAME: "Beyond All Reason test-30707-555e3a1" + GAME: "Beyond All Reason test-30770-4e0b241" HASH_FILE: "synctest_synchash.json" jobs: diff --git a/test/synctest/README.md b/test/synctest/README.md index 30af8a81fef..3bc32d08350 100644 --- a/test/synctest/README.md +++ b/test/synctest/README.md @@ -28,7 +28,7 @@ sed -e "s/@VERSION@/${GAME##* }/g" -e "s/@MAPNAME@/$MAP/g" \ spring-headless --isolation --write-dir ./bar-data startscript.txt ``` -A successful run exits 0 at game frame ~2100 (the startscript's trailing `quitforce`) and leaves `bar-data/synctest_synchash.json` — a JSON file whose `digest` field is what CI compares. +A successful run exits 0 at game frame ~2200 (the startscript's trailing `quitforce`) and leaves `bar-data/synctest_synchash.json` — a JSON file whose `digest` field is what CI compares. ## Bumping the pinned BAR version @@ -40,3 +40,9 @@ BAR's rapid `test--` tags are content-addressed — once published they The startscript was generated by BAR from running a fightertest scenario in the actual BAR lobby, then pulled from the bar data dir; modifying it to instead run synctest on a different map. If it ever needs changing, that's the easiest way to regenerate it. A trailing `quitforce` in the startscript's `debugcommands` is required so the engine actually quits when the test finishes. + +## Why `luaui disable` + +`debugcommands` disables LuaUI at frame 4, before any units are spawned. This is required for the test to be reproducible. + +BAR's LuaUI widgets are unsynced but issue orders into the simulation, so for example `unit_bombers_default_hold_fire.lua` calls `Spring.GiveOrderToUnit` for every bomber created. Orders issued from unsynced code become network commands, so the frame on which they take effect depends on wall-clock scheduling. Two runs with identical inputs then diverge: a bomber flips to Hold Fire on frame 53 in one run and frame 54 in the next, and everything downstream follows. diff --git a/test/synctest/synctest-startscript.txt b/test/synctest/synctest-startscript.txt index ce644af5d6e..95cd3c3356f 100644 --- a/test/synctest/synctest-startscript.txt +++ b/test/synctest/synctest-startscript.txt @@ -26,7 +26,7 @@ } [modoptions] { - debugcommands=2:cheat|3:globallos|15:luarules synctest 2000 0.33 0.33 0.1 0.5|20:setspeed 20|21:speedcontrol 0|2100:quitforce; + debugcommands=2:cheat|3:globallos|4:luaui disable|15:luarules synctest 2000 0.33 0.33 0.1 0.5 100|20:setspeed 20|21:speedcontrol 0|2200:quitforce; } [player0] { From bc4b39d7a91a40c208dc9e69a734ee20ca26d56e Mon Sep 17 00:00:00 2001 From: Tom J Nowell Date: Mon, 27 Jul 2026 02:15:21 +0100 Subject: [PATCH 64/90] Use a portable type cast in MemPoolTypes logging --- rts/System/MemPoolTypes.h | 2 +- rts/System/Platform/Threading.h | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/rts/System/MemPoolTypes.h b/rts/System/MemPoolTypes.h index dcd5730e040..3ab106b64b9 100644 --- a/rts/System/MemPoolTypes.h +++ b/rts/System/MemPoolTypes.h @@ -431,7 +431,7 @@ inline size_t StablePosAllocator::Allocate(size_t numElems) if (positionToSize.empty()) { size_t returnPos = data.size(); data.resize(data.size() + numElems); - myLog("StablePosAllocator::Allocate(%u) = %u [thread_id = %u]", uint32_t(numElems), uint32_t(returnPos), static_cast(Threading::GetCurrentThreadId())); + myLog("StablePosAllocator::Allocate(%u) = %u [thread_id = %u]", uint32_t(numElems), uint32_t(returnPos), Threading::GetCurrentThreadIdAsU32()); return returnPos; } diff --git a/rts/System/Platform/Threading.h b/rts/System/Platform/Threading.h index e2d94292b59..277f49f7643 100644 --- a/rts/System/Platform/Threading.h +++ b/rts/System/Platform/Threading.h @@ -18,6 +18,7 @@ #include #include +#include #include #include @@ -55,6 +56,19 @@ namespace Threading { NativeThreadHandle GetCurrentThread(); NativeThreadId GetCurrentThreadId(); + // Convert a NativeThreadId to a 32-bit value for logging. NativeThreadId is a + // pointer (pthread_t) on macOS/BSD and an integer elsewhere; the template lets + // `if constexpr` select a cast that is valid for the actual type on each target + // (it would not be discarded in a non-template function). + template + inline std::uint32_t ThreadIdAsU32(TId id) { + if constexpr (std::is_pointer_v) + return static_cast(reinterpret_cast(id)); + else + return static_cast(id); + } + inline std::uint32_t GetCurrentThreadIdAsU32() { return ThreadIdAsU32(GetCurrentThreadId()); } + #ifndef _WIN32 extern thread_local std::shared_ptr localThreadControls; #endif From 78a44b877586c0874e562794e76d8336b1578480 Mon Sep 17 00:00:00 2001 From: lostsquirrel1 Date: Mon, 27 Jul 2026 18:03:12 +0100 Subject: [PATCH 65/90] Update changelogs, creating a new log for 2026-06. --- doc/site/content/changelogs/_index.markdown | 182 +++++++++--------- .../changelogs/changelog-2026-06.markdown | 97 ++++++++++ 2 files changed, 192 insertions(+), 87 deletions(-) create mode 100644 doc/site/content/changelogs/changelog-2026-06.markdown diff --git a/doc/site/content/changelogs/_index.markdown b/doc/site/content/changelogs/_index.markdown index 993a030f907..b0d35bc0870 100644 --- a/doc/site/content/changelogs/_index.markdown +++ b/doc/site/content/changelogs/_index.markdown @@ -5,93 +5,101 @@ title = "Running changelog" type = "docs" +++ -This is the bleeding-edge changelog since version 2025.06, for **pre-release 2026.06**. +This is the bleeding-edge changelog since version 2026.06, for **pre-release 2026.07**. ## Caveats -- UTF-8 file paths are now supported. -- some file accesses are now case-sensitive. -- rmlUI version used 6.0 → 6.2 -- ARM64 architecture builds now have nominal support. -- `/aicontrol` is now blocked by default. Call `/aiCtrl PlayerName` or `/aiCtrlByNum 123` to enable. -- `script:AimWeapon` now receives unit-relative heading and pitch, rather than world-space. This means units angled on slopes will receive different values. -- heading cast to radians will now return \[-pi; +pi) rather than \[0; tau). -- minor Lua env sandboxing changes, see the "Lua environment sandboxing" section below. -- always output logs to stdout. -- removed `CSphereParticleSpawner` particle class. Identical to `CSimpleParticleSystem`. -- archive cache version 20 → 21. - -## Features - -### RmlUi - -- rmlUI version used 6.0 → 6.2 -- add datamodel support for pairs: `pairs(dm_handle)` -- add datamodel support for ipairs: `dm_handle:__ipairs()` -- support for accessing the underlying datamodel table with `dm_handle.__raw()` -- allow datamodel self-referential assignments such as `dm_handle.property = dm_handle.another_property` -- support for retrieving datamodel property length: `dm_handle.property.__len()` -- fix datamodel array access -- fix `data-value` binds in rml elements -- added `RmlUi.GetDocumentPathRequests(string docPath) -> {"filePath", "filePath", ...}` which tracks all of the files opened by an RmlUi LoadDocument call -- added `RmlUi.ClearDocumentPathRequests(string docPath) -> nil` to clear tracked LoadDocument files - -### Radar icon Lua API -- added `Spring.SetUnitIcon(unitID, string? iconName)`. Pass `nil` to reset to default. -- added `Spring.GetUnitIcon(unitID) → string iconName`. - -### Minimap callins -- added `wupget:MiniMapRotationChanged(rotation, previousRotation)` unsynced callin. In radians. -- added `wupget:MiniMapGeometryChanged(x, y, sizeX, sizeY, prevX, prevY, prevSizeX, prevSizeY)` unsynced callin. In pixels. -- added `wupget:MiniMapStateChanged(isMinimized, isMaximized, isSlaved)` unsynced callin. - -### Lua environment sandboxing -- LuaSocket no longer inits before Lua sandboxing. -- unsynced LuaRules (incl. unsynced LuaGaia) now has access to `io` and `os` libraries. -- unsynced LuaRules (incl. unsynced LuaGaia) now has access to the `debug` library by default (no longer requires devmode). - -### Custom teamcolor palette -- add `Spring.SetCustomPaletteColor(paletteID, r, g, b) → nil`. Sets a palette color for shader use. See below. -- add `Spring.GetCustomPaletteColor(paletteID) → r, g, b`. -- add `Engine.maxCustomPaletteID`, the highest available palette ID. -- add `Spring.SetUnitPaletteIndex(unitID, paletteID?) → nil`. Assigns a paletteID to a unit. Use nil to reset to the default palette. -- add `Spring.GetUnitPaletteIndex(unitID) → paletteID?`. -- add `Spring.SetFeaturePaletteIndex(featureID, paletteID?)`. -- add `Spring.GetFeaturePaletteIndex(featureID) → paletteID?`. -- the `teamColor` array in shaders now has much more room and contains both teamcolors and colors of the custom palette, as per above. -- the actual teamID is now available as the fifth byte (first byte of the second 4-byte composite) in model uniform data. -- note that the value of the palette index is different than what is seen from Lua. Units with no custom paletteID have the index point to an entry that contains their team color. -- the basecontent teamcolor shader takes the above changes into account. Existing custom shaders that use `instData.z & 0xFF` for teamID should keep working as long as the palette feature isn't used, to support it properly the constant needs to be `0x7FF` instead. -- projectiles, and ghosts (buildings out of sight and queued things) unchanged, they still just draw teamcolor naively and cannot use a shader. - -### Replay path getters -- add `Spring.GetReplayFilePath() → string?`, returns path of replay being watched. -- add `Spring.GetReplayRecordingFilePath() → string?`, returns path of replay to be produced. Note that this is just a prospective file path (nothing is written until the match ends), and that it possible to record a replay of a replay. - -### Build commands -- builders now perform an extra block check immediately when a build command reaches the front of the queue. This is in addition to the existing periodic (≈ 0.4 Hz) block check. A block check cancels a build command if the build site is hard-blocked ("red squares", as opposed to "yellow squares" with reclaimables/mobiles). -- add `Spring.SetEngineBuildSquareRendering(bool) → nil`, for disabling the native rendering of the footprint grid when a build command is selected. -- add `wupget:DrawBuildSquare(unitDefID, x, z, facing, statuses) → nil` unsynced callin, fires when a build command is selected. Statuses is a 1D array for the status of each tile: 0 blocked (red), 1 occupied (yellow), 2 reclaimable (yellow), 3 open (green). This fires even if native drawing is enabled. - -### Misc - -- UTF-8 file paths are now supported. -- some file accesses are now case-sensitive. -- `/aicontrol` is now blocked by default. Call `/aiCtrl PlayerName` or `/aiCtrlByNum 123` to enable. -- `script:AimWeapon` now receives unit-relative heading and pitch, rather than world-space. This means units angled on slopes will receive different values. -- heading cast to radians will now return \[-pi; +pi) rather than \[0; tau). -- `VFS.GetAvailableAIs()` returned entries now have a new `isLuaAI` boolean. -- add `Platform.architecture`, string. Usually "x86_64", with some ongoing work to support "arm64". -- add `Spring.SetCheatingEnabled(bool)`. -- add `Spring.SetGodMode(bool? controlAllies, bool? controlEnemies)`. -- add `Spring.GetClosestEnemyUnit(x, y, z, range = inf) → unitID?` to LuaUI. -- add `Spring.GetClosestEnemyUnit(x, y, z, range = inf, allyTeamID, bool useLoS = true, bool spherical = false, bool requireEnemyToSeePos = false) → unitID?` to LuaRules. -- large QTPFS perf improvements. -- always output logs to stdout. -- add boolean `Platform.isHeadless`. -- archive cache version 20 → 21. - -## Fixes - -- fix logs sometimes not getting flushed on exit -- fix `CMD[20]` and `CMD[105]` returning legacy aliases for those commands rather than their standard names. +## Build +- Fixes for MSVC build +- Added warning about unsynced git submodules [PR 2902](https://github.com/beyond-all-reason/RecoilEngine/pull/2902) +- Fixed compilation warnings in CFrontTexture.cpp +- Fixed many compiler warnings +- Fixed sse2neon/streflop macro redefinition warning +- Build/compile changes to lead to eventual support for MacOS +- Added GetPrevFrameChecksum() to the Lua API +- Add multi-platform sync testing (amd64-windows/linux + arm64-linux) [PR 2921](https://github.com/beyond-all-reason/RecoilEngine/pull/2921) +- Optimize LuaPushNamedFoo using compile time key hashing [PR 2986](https://github.com/beyond-all-reason/RecoilEngine/pull/2986) +- fix(synctest): workaround widget timing nondeterminism [PR 3124](https://github.com/beyond-all-reason/RecoilEngine/pull/3124) + +## Documentation +- Added 'First Steps with the Engine' guide for game developers +- Fixed spGetUnitNearestEnemy docs +- UnitDestroyed attacker not always available [PR 2572](https://github.com/beyond-all-reason/RecoilEngine/pull/2572) +- Fixed wupget:DefaultCommand missing cmdID [PR 2984](https://github.com/beyond-all-reason/RecoilEngine/pull/2984) +- Fixed SplinterFaction card link format [PR 2999](https://github.com/beyond-all-reason/RecoilEngine/pull/2999) +- Added ENGINE_PERFORMANCE.md with some notes on engine internals [PR 2919](https://github.com/beyond-all-reason/RecoilEngine/pull/2919) +- Added BACKWARDS_COMPATIBILITY.md guidance doc for coding agents +- Improved running/testing instructions in AGENTS.md [PR 2917](https://github.com/beyond-all-reason/RecoilEngine/pull/2917) +- Spelling correction pass across site articles + +## Lua +- LuaSocket no longer inits before sandboxing +- Extracted synced lib loader to a helper function +- Unsynced wrapper handles its sandboxing +- VFS.GetAvailableAIs: add isLuaAI +- Enabled IO, OS and debug libraries for unsynced gadgets [PR 2858](https://github.com/beyond-all-reason/RecoilEngine/pull/2858) +- Added Lua API to get current replay file paths +- Fixed memory barrier bitfield now pulls from correct parameter [PR 2557](https://github.com/beyond-all-reason/RecoilEngine/pull/2557) +- Implement DrawBuildSquare callin and add example GL4 widget [PR 2938](https://github.com/beyond-all-reason/RecoilEngine/pull/2938) +- Added Platform.architecture [PR 2825](https://github.com/beyond-all-reason/RecoilEngine/pull/2825) +- SolLua: use sol::lua_nil instead of the sol::nil alias +- LuaTextures: log glTexImage failures instead of returning nil silently +- Added Lua trace ray functions [PR 1624](https://github.com/beyond-all-reason/RecoilEngine/pull/1624) +- Added type checking to ADD_FOO Lua defs macros +- Fixed LuaSocket VFS mode error in LuaMenu +- Updated Spring.SetSkyBoxTexture to perform setup if needed +- LuaParser: run Spring.TimeCheck's callback under unitsync/dedicated +- Added debug.emulateFoo input emulation API [PR 3097](https://github.com/beyond-all-reason/RecoilEngine/pull/3097) +- Added gadget:ResourceExcess(excessTable) -> bool +- Added spAddTeamResourceExcessStats +- Added debug.emulateMouseWheel input emulation +- Added cancelcommand action +- Added GetPrevFrameChecksum() to the Lua API [PR 2922](https://github.com/beyond-all-reason/RecoilEngine/pull/2922) +- Fixes to Spring.SetMapShader [PR 3127](https://github.com/beyond-all-reason/RecoilEngine/pull/3127) + +## Misc +- Restored lowercasing in FileSystem::GetExtension +- Demo handlers improvements: keep file paths absolute until needed and do not add a fake 'unnamed.sdfz' name to recordings +- Fixed missing cstdint include in ChatMessage.h [PR 2968](https://github.com/beyond-all-reason/RecoilEngine/pull/2968) +- Fixed the Nonus backlash scancode spelling mistake. [Issue 2978](https://github.com/beyond-all-reason/RecoilEngine/issues/2978) +- Fixed FileSystem::FindFiles so it should return relative paths [PR 3007](https://github.com/beyond-all-reason/RecoilEngine/pull/3007) +- float3: drop redundant direct streflop_cond.h include +- SafeUtil: include and directly [PR 3025](https://github.com/beyond-all-reason/RecoilEngine/pull/3025) +- Logging fix: updated section min-level in place instead of appending duplicates [PR 3052](https://github.com/beyond-all-reason/RecoilEngine/pull/3052) +- Converted u8string_view to string using explicit size +- Fixde inconsistent class/struct forward declarations +- Fixed truncated sync checksum in demotool dump +- Improve cross-platform portability in archive handlers +- Removed legacy engine options +- SpringMath: add Catmull-Rom bicubic interpolation helpers +- Moved float3/float4/Matrix44f str() out of line +- Fixed unbindaction not clearing scancode bindings +- AIFloat3: Remove non-trivial copy constructor +- Fix /unbind for keychains +- Fix stale spGetActionHotKeys returns [PR 3082](https://github.com/beyond-all-reason/RecoilEngine/pull/3082) +- Use a portable type cast in MemPoolTypes logging + +## Rendering +- Added sorting icon names before adding to atlas so insertion order is consistent across runs. +- Replaced non-deterministic GL texture IDs with stable insertion-order indices for UniqueSubTexture naming. +- Fixed stableIdx assignment to be per-file at first insertion time. +- Also sorted icon rendering to use deterministic ProjectileDrawer iteration. +- Added specifying output format (e.g. "dumpatlas proj tga") instead of hardcoded .png. +- Fixed icon sliding artifact and add edge-pixel padding to atlas +- Disabled runniung atlas/iconhandler in headless +- Fixed a rare crash in UnitDrawer / IconsDrawer caused by inconsistent LOS settings set from Lua. +- Added Custom Color Palette [PR 2945](https://github.com/beyond-all-reason/RecoilEngine/pull/2945) +- Fixed to icons bleeding: adjust CTextureRenderAtlas so it always works with the original/biggest lod +- Removed Java AI bindings +- Debug logs for loading splat normals +- Fix SMF DNTS gating and fallback textures +- Add mapoptions for blank map splats + +## Simulation +- Sanitize NaNs in CHoverAirMoveType::UpdateMoveRate() +- Set a minimum camera controller height +- Make invalid buildoption warning more verbose +- Early block check for build commands [PR 2557](https://github.com/beyond-all-reason/RecoilEngine/pull/2557) +- Fixed to guard against already-dead reclaim targets [PR 3020](https://github.com/beyond-all-reason/RecoilEngine/pull/3020) +- Fixed edge scrolling threshold [Issue 2987](https://github.com/beyond-all-reason/RecoilEngine/issues/2987) +- Dump state handles resource packs diff --git a/doc/site/content/changelogs/changelog-2026-06.markdown b/doc/site/content/changelogs/changelog-2026-06.markdown new file mode 100644 index 00000000000..993a030f907 --- /dev/null +++ b/doc/site/content/changelogs/changelog-2026-06.markdown @@ -0,0 +1,97 @@ ++++ +title = "Running changelog" +[cascade] + [cascade.params] + type = "docs" ++++ + +This is the bleeding-edge changelog since version 2025.06, for **pre-release 2026.06**. + +## Caveats + +- UTF-8 file paths are now supported. +- some file accesses are now case-sensitive. +- rmlUI version used 6.0 → 6.2 +- ARM64 architecture builds now have nominal support. +- `/aicontrol` is now blocked by default. Call `/aiCtrl PlayerName` or `/aiCtrlByNum 123` to enable. +- `script:AimWeapon` now receives unit-relative heading and pitch, rather than world-space. This means units angled on slopes will receive different values. +- heading cast to radians will now return \[-pi; +pi) rather than \[0; tau). +- minor Lua env sandboxing changes, see the "Lua environment sandboxing" section below. +- always output logs to stdout. +- removed `CSphereParticleSpawner` particle class. Identical to `CSimpleParticleSystem`. +- archive cache version 20 → 21. + +## Features + +### RmlUi + +- rmlUI version used 6.0 → 6.2 +- add datamodel support for pairs: `pairs(dm_handle)` +- add datamodel support for ipairs: `dm_handle:__ipairs()` +- support for accessing the underlying datamodel table with `dm_handle.__raw()` +- allow datamodel self-referential assignments such as `dm_handle.property = dm_handle.another_property` +- support for retrieving datamodel property length: `dm_handle.property.__len()` +- fix datamodel array access +- fix `data-value` binds in rml elements +- added `RmlUi.GetDocumentPathRequests(string docPath) -> {"filePath", "filePath", ...}` which tracks all of the files opened by an RmlUi LoadDocument call +- added `RmlUi.ClearDocumentPathRequests(string docPath) -> nil` to clear tracked LoadDocument files + +### Radar icon Lua API +- added `Spring.SetUnitIcon(unitID, string? iconName)`. Pass `nil` to reset to default. +- added `Spring.GetUnitIcon(unitID) → string iconName`. + +### Minimap callins +- added `wupget:MiniMapRotationChanged(rotation, previousRotation)` unsynced callin. In radians. +- added `wupget:MiniMapGeometryChanged(x, y, sizeX, sizeY, prevX, prevY, prevSizeX, prevSizeY)` unsynced callin. In pixels. +- added `wupget:MiniMapStateChanged(isMinimized, isMaximized, isSlaved)` unsynced callin. + +### Lua environment sandboxing +- LuaSocket no longer inits before Lua sandboxing. +- unsynced LuaRules (incl. unsynced LuaGaia) now has access to `io` and `os` libraries. +- unsynced LuaRules (incl. unsynced LuaGaia) now has access to the `debug` library by default (no longer requires devmode). + +### Custom teamcolor palette +- add `Spring.SetCustomPaletteColor(paletteID, r, g, b) → nil`. Sets a palette color for shader use. See below. +- add `Spring.GetCustomPaletteColor(paletteID) → r, g, b`. +- add `Engine.maxCustomPaletteID`, the highest available palette ID. +- add `Spring.SetUnitPaletteIndex(unitID, paletteID?) → nil`. Assigns a paletteID to a unit. Use nil to reset to the default palette. +- add `Spring.GetUnitPaletteIndex(unitID) → paletteID?`. +- add `Spring.SetFeaturePaletteIndex(featureID, paletteID?)`. +- add `Spring.GetFeaturePaletteIndex(featureID) → paletteID?`. +- the `teamColor` array in shaders now has much more room and contains both teamcolors and colors of the custom palette, as per above. +- the actual teamID is now available as the fifth byte (first byte of the second 4-byte composite) in model uniform data. +- note that the value of the palette index is different than what is seen from Lua. Units with no custom paletteID have the index point to an entry that contains their team color. +- the basecontent teamcolor shader takes the above changes into account. Existing custom shaders that use `instData.z & 0xFF` for teamID should keep working as long as the palette feature isn't used, to support it properly the constant needs to be `0x7FF` instead. +- projectiles, and ghosts (buildings out of sight and queued things) unchanged, they still just draw teamcolor naively and cannot use a shader. + +### Replay path getters +- add `Spring.GetReplayFilePath() → string?`, returns path of replay being watched. +- add `Spring.GetReplayRecordingFilePath() → string?`, returns path of replay to be produced. Note that this is just a prospective file path (nothing is written until the match ends), and that it possible to record a replay of a replay. + +### Build commands +- builders now perform an extra block check immediately when a build command reaches the front of the queue. This is in addition to the existing periodic (≈ 0.4 Hz) block check. A block check cancels a build command if the build site is hard-blocked ("red squares", as opposed to "yellow squares" with reclaimables/mobiles). +- add `Spring.SetEngineBuildSquareRendering(bool) → nil`, for disabling the native rendering of the footprint grid when a build command is selected. +- add `wupget:DrawBuildSquare(unitDefID, x, z, facing, statuses) → nil` unsynced callin, fires when a build command is selected. Statuses is a 1D array for the status of each tile: 0 blocked (red), 1 occupied (yellow), 2 reclaimable (yellow), 3 open (green). This fires even if native drawing is enabled. + +### Misc + +- UTF-8 file paths are now supported. +- some file accesses are now case-sensitive. +- `/aicontrol` is now blocked by default. Call `/aiCtrl PlayerName` or `/aiCtrlByNum 123` to enable. +- `script:AimWeapon` now receives unit-relative heading and pitch, rather than world-space. This means units angled on slopes will receive different values. +- heading cast to radians will now return \[-pi; +pi) rather than \[0; tau). +- `VFS.GetAvailableAIs()` returned entries now have a new `isLuaAI` boolean. +- add `Platform.architecture`, string. Usually "x86_64", with some ongoing work to support "arm64". +- add `Spring.SetCheatingEnabled(bool)`. +- add `Spring.SetGodMode(bool? controlAllies, bool? controlEnemies)`. +- add `Spring.GetClosestEnemyUnit(x, y, z, range = inf) → unitID?` to LuaUI. +- add `Spring.GetClosestEnemyUnit(x, y, z, range = inf, allyTeamID, bool useLoS = true, bool spherical = false, bool requireEnemyToSeePos = false) → unitID?` to LuaRules. +- large QTPFS perf improvements. +- always output logs to stdout. +- add boolean `Platform.isHeadless`. +- archive cache version 20 → 21. + +## Fixes + +- fix logs sometimes not getting flushed on exit +- fix `CMD[20]` and `CMD[105]` returning legacy aliases for those commands rather than their standard names. From eb117a248ff5a52b3c3964032e79a9f9595e39cb Mon Sep 17 00:00:00 2001 From: Tom J Nowell Date: Mon, 27 Jul 2026 22:33:57 +0100 Subject: [PATCH 66/90] CMake: drop redundant simdjson include dir, rely on link (#3077) Every consumer already links simdjson::simdjson (GLTFParser via engineCommonLibraries; fastgltf links it privately), and that in-tree target exports the vendored include as a normal -I dir. DevIL's imported target is the only thing putting /opt/homebrew/include on the line, and as an imported target its include is emitted -isystem, which the compiler searches after every -I dir - so the vendored simdjson header wins on its own. The manual include_directories entry was redundant. Per review feedback (p2004a). --- rts/CMakeLists.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/rts/CMakeLists.txt b/rts/CMakeLists.txt index 001ea7cb6ba..ae47f2aa4a8 100644 --- a/rts/CMakeLists.txt +++ b/rts/CMakeLists.txt @@ -142,7 +142,6 @@ make_global_var(engineSources ${sources_engine_ExternalAI} ) include_directories(${CMAKE_CURRENT_SOURCE_DIR}/lib/assimp/include) #FIXME: hack for rts/Rendering/Models/IModelParser.cpp -include_directories(${CMAKE_CURRENT_SOURCE_DIR}/lib/simdjson/include) include_directories(${CMAKE_CURRENT_SOURCE_DIR}/lib/fastgltf/include) ### Add headers for generated project files (e.g. Code::Blocks) From ae7a172cf8fb5e5ec1fbd7fb77601d6626ff28a0 Mon Sep 17 00:00:00 2001 From: shawjef3 Date: Tue, 28 Jul 2026 05:35:09 -0400 Subject: [PATCH 67/90] Fix units having the wrong path id after loading a save game. (#3120) --- rts/Sim/MoveTypes/GroundMoveType.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/rts/Sim/MoveTypes/GroundMoveType.cpp b/rts/Sim/MoveTypes/GroundMoveType.cpp index 066dea2f634..8790ce9eb63 100644 --- a/rts/Sim/MoveTypes/GroundMoveType.cpp +++ b/rts/Sim/MoveTypes/GroundMoveType.cpp @@ -146,8 +146,11 @@ CR_REG_METADATA(CGroundMoveType, ( CR_MEMBER(forceFromStaticCollidees), CR_MEMBER(pathID), - CR_MEMBER(nextPathId), - CR_MEMBER(deletePathId), + // The ECS registry is not persisted across save/load, so a saved entity ID will collide with + // a newly-allocated entity in the fresh registry, causing FollowPath to destroy the wrong path + // or swap to an invalid one. + CR_IGNORED(nextPathId), + CR_IGNORED(deletePathId), CR_MEMBER(numIdlingUpdates), CR_MEMBER(numIdlingSlowUpdates), From fcafdea110e826ab4b6d07578124479148491be6 Mon Sep 17 00:00:00 2001 From: lostsquirrel1 Date: Tue, 28 Jul 2026 10:40:42 +0100 Subject: [PATCH 68/90] Update changelog for PR 3120 --- doc/site/content/changelogs/_index.markdown | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/site/content/changelogs/_index.markdown b/doc/site/content/changelogs/_index.markdown index b0d35bc0870..dfbde9307f9 100644 --- a/doc/site/content/changelogs/_index.markdown +++ b/doc/site/content/changelogs/_index.markdown @@ -103,3 +103,4 @@ This is the bleeding-edge changelog since version 2026.06, for **pre-release 202 - Fixed to guard against already-dead reclaim targets [PR 3020](https://github.com/beyond-all-reason/RecoilEngine/pull/3020) - Fixed edge scrolling threshold [Issue 2987](https://github.com/beyond-all-reason/RecoilEngine/issues/2987) - Dump state handles resource packs +- Fix units having the wrong path id after loading a save game. [PR 3120](https://github.com/beyond-all-reason/RecoilEngine/pull/3120) From d16867bc71c26912f06d7e7312fec56dad9428f8 Mon Sep 17 00:00:00 2001 From: Bartlomiej Komendarczuk Date: Tue, 28 Jul 2026 12:53:42 +0200 Subject: [PATCH 69/90] Support alternate file extensions for replays (#2975) * Support alternate file extensions for replays --------- Co-authored-by: sprunk --- rts/Menu/SelectionWidget.cpp | 11 +- rts/System/CMakeLists.txt | 1 + rts/System/LoadSave/DemoFileExtension.cpp | 57 +++++ rts/System/LoadSave/DemoFileExtension.h | 10 + rts/System/LoadSave/DemoReader.cpp | 6 +- rts/System/LoadSave/DemoRecorder.cpp | 7 +- rts/System/SpringApp.cpp | 11 +- rts/builds/dedicated/CMakeLists.txt | 1 + test/CMakeLists.txt | 17 ++ .../System/LoadSave/testDemoFileExtension.cpp | 205 ++++++++++++++++++ 10 files changed, 320 insertions(+), 6 deletions(-) create mode 100644 rts/System/LoadSave/DemoFileExtension.cpp create mode 100644 rts/System/LoadSave/DemoFileExtension.h create mode 100644 test/engine/System/LoadSave/testDemoFileExtension.cpp diff --git a/rts/Menu/SelectionWidget.cpp b/rts/Menu/SelectionWidget.cpp index 7fbbf7b64f4..058dbebb40c 100644 --- a/rts/Menu/SelectionWidget.cpp +++ b/rts/Menu/SelectionWidget.cpp @@ -1,6 +1,7 @@ /* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ #include "SelectionWidget.h" +#include "System/LoadSave/DemoFileExtension.h" ///[maint]#ifndef HEADLESS #include @@ -104,7 +105,15 @@ void SelectionWidget::ShowDemoList(const std::function const std::string dir = FileSystem::EnsurePathSepAtEnd("demos"); // FIXME: names overflow the box - for (const std::string& demo: dataDirsAccess.FindFiles(cwd + dir, "*.sdfz", 0)) { + const auto exts = GetDemoFileExtensions(); + std::string joined; + for (const auto& ext : exts) { + if (!joined.empty()) + joined += ','; + joined += ext; + } + const auto pattern = std::format("*.{{{}}}", joined); + for (const std::string& demo : dataDirsAccess.FindFiles(cwd + dir, pattern, 0)) { curSelect->list->AddItem(demo.substr(demo.find(dir) + 6), ""); } diff --git a/rts/System/CMakeLists.txt b/rts/System/CMakeLists.txt index c8fae324574..00022fa567b 100644 --- a/rts/System/CMakeLists.txt +++ b/rts/System/CMakeLists.txt @@ -27,6 +27,7 @@ make_global_var(sources_engine_System_common "${CMAKE_CURRENT_SOURCE_DIR}/Input/MouseInput.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/LoadSave/CregLoadSaveHandler.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/LoadSave/Demo.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/LoadSave/DemoFileExtension.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/LoadSave/DemoReader.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/LoadSave/DemoRecorder.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/LoadSave/LoadSaveHandler.cpp" diff --git a/rts/System/LoadSave/DemoFileExtension.cpp b/rts/System/LoadSave/DemoFileExtension.cpp new file mode 100644 index 00000000000..ee821b169fa --- /dev/null +++ b/rts/System/LoadSave/DemoFileExtension.cpp @@ -0,0 +1,57 @@ +/* This file is part of the Recoil engine (GPL v2 or later), see LICENSE.html */ + +#include "DemoFileExtension.h" + +#include +#include +#include +#include + +#include + +#include "demofile.h" +#include "System/Config/ConfigHandler.h" +#include "System/StringUtil.h" + +CONFIG(std::string, DemoFileExtension).defaultValue("sdfz").description("Comma-separated list of replay file extensions. The first entry is used when recording; all entries are accepted when loading. Set by the lobby (e.g. 'barreplay,sdfz' for BAR)."); + +std::vector GetDemoFileExtensions() +{ + const auto configValue = configHandler->GetString("DemoFileExtension"); + + std::vector extensions; + + for (const auto& part : configValue | std::views::split(',')) { + auto extension = StringTrim(std::string(part.begin(), part.end())); + + if (!extension.empty() && extension.find_first_of("/\\.") == std::string::npos) + extensions.emplace_back(std::move(extension)); + } + + if (extensions.empty()) + extensions.emplace_back("sdfz"); + return extensions; +} + +bool IsDemoExtension(const std::string& ext) +{ +#ifdef TOOLS + return true; +#else + std::string lower = ext; + std::transform(lower.begin(), lower.end(), lower.begin(), [](unsigned char c) { return std::tolower(c); }); + const auto extensions = GetDemoFileExtensions(); + return std::ranges::contains(extensions, lower); +#endif +} + +bool ContentsLookLikeAReplay(const std::string& path) +{ + gzFile file = gzopen(path.c_str(), "rb"); + if (file == nullptr) + return false; + decltype(DemoFileHeader::magic) magic = {}; + const int bytesRead = gzread(file, magic, sizeof(magic)); + gzclose(file); + return (bytesRead == static_cast(sizeof(magic)) && memcmp(magic, DEMOFILE_MAGIC, sizeof(magic)) == 0); +} diff --git a/rts/System/LoadSave/DemoFileExtension.h b/rts/System/LoadSave/DemoFileExtension.h new file mode 100644 index 00000000000..b620bd28afc --- /dev/null +++ b/rts/System/LoadSave/DemoFileExtension.h @@ -0,0 +1,10 @@ +/* This file is part of the Recoil engine (GPL v2 or later), see LICENSE.html */ + +#pragma once + +#include +#include + +std::vector GetDemoFileExtensions(); +bool IsDemoExtension(const std::string& ext); +bool ContentsLookLikeAReplay(const std::string& path); diff --git a/rts/System/LoadSave/DemoReader.cpp b/rts/System/LoadSave/DemoReader.cpp index d999bb32a55..b3b451feb10 100644 --- a/rts/System/LoadSave/DemoReader.cpp +++ b/rts/System/LoadSave/DemoReader.cpp @@ -6,9 +6,11 @@ #include "Sim/Misc/GlobalConstants.h" #ifndef TOOLS +#include "DemoFileExtension.h" #include "System/Config/ConfigHandler.h" CONFIG(bool, DisableDemoVersionCheck).defaultValue(false).description("Allow to play every replay file (may crash / cause undefined behaviour in replays)"); #endif + #include "System/Exceptions.h" #include "System/FileSystem/GZFileHandler.h" #include "System/FileSystem/FileSystem.h" @@ -53,8 +55,8 @@ CDemoReader::CDemoReader(const std::string& filename, float curTime): playbackDe { demoName = filename; - if (FileSystem::GetExtensionLowerCase(filename) != "sdfz") - throw content_error("Unknown demo extension: " + FileSystem::GetExtensionLowerCase(filename)); + if (!IsDemoExtension(FileSystem::GetExtensionLowerCase(filename))) + throw content_error("Unsupported demo extension: " + FileSystem::GetExtensionLowerCase(filename)); // file not found -> exception if (!playbackDemo->FileExists()) diff --git a/rts/System/LoadSave/DemoRecorder.cpp b/rts/System/LoadSave/DemoRecorder.cpp index 9ee78245b51..55e3781e2b2 100644 --- a/rts/System/LoadSave/DemoRecorder.cpp +++ b/rts/System/LoadSave/DemoRecorder.cpp @@ -6,11 +6,13 @@ #include #include "DemoRecorder.h" +#include "DemoFileExtension.h" #include "base64.h" #include "Game/GameVersion.h" #include "Sim/Misc/TeamStatistics.h" #include "System/TimeUtil.h" #include "System/StringUtil.h" +#include "System/Config/ConfigHandler.h" #include "System/FileSystem/DataDirsAccess.h" #include "System/FileSystem/FileSystem.h" #include "System/FileSystem/FileQueryFlags.h" @@ -160,12 +162,13 @@ void CDemoRecorder::SetName(const std::string& mapName, const std::string& modNa // oss << FileSystem::GetBasename(modName); // oss << "_"; oss << engineVersionName; - buf << oss.str() << ".sdfz"; + const std::string ext = "." + GetDemoFileExtensions()[0]; + buf << oss.str() << ext; int n = 0; while (FileSystem::FileExists(buf.str()) && (n < 99)) { buf.str(""); // clears content - buf << oss.str() << "_" << n++ << ".sdfz"; + buf << oss.str() << "_" << n++ << ext; } demoName = buf.str(); diff --git a/rts/System/SpringApp.cpp b/rts/System/SpringApp.cpp index cacaf260b78..fd0a928910b 100644 --- a/rts/System/SpringApp.cpp +++ b/rts/System/SpringApp.cpp @@ -100,6 +100,7 @@ #include "System/Sound/ISound.h" #include "System/Sync/FPUCheck.h" #include "System/Threading/ThreadPool.h" +#include "System/LoadSave/DemoFileExtension.h" #include "Game/UnsyncedGameCommands.h" #include "Game/SyncedGameCommands.h" @@ -686,6 +687,14 @@ void SpringApp::LoadSpringMenu() } } +static bool IsReplay(const std::string& path) +{ + if (IsDemoExtension(FileSystem::GetExtensionLowerCase(path))) + return true; + + return ContentsLookLikeAReplay(path); +} + /** * Initializes instance of GameSetup */ @@ -732,7 +741,7 @@ void SpringApp::Startup() pregame = new CPreGame(clientSetup); return; } - if (extension == "sdfz") { + if (IsReplay(inputFile)) { LoadDemoFile(inputFile); return; } diff --git a/rts/builds/dedicated/CMakeLists.txt b/rts/builds/dedicated/CMakeLists.txt index f425d68bd4f..51548bac7ca 100644 --- a/rts/builds/dedicated/CMakeLists.txt +++ b/rts/builds/dedicated/CMakeLists.txt @@ -102,6 +102,7 @@ set(system_files ${ENGINE_SRC_ROOT_DIR}/System/Config/ConfigSource.cpp ${ENGINE_SRC_ROOT_DIR}/System/Config/ConfigVariable.cpp ${ENGINE_SRC_ROOT_DIR}/System/LoadSave/Demo.cpp + ${ENGINE_SRC_ROOT_DIR}/System/LoadSave/DemoFileExtension.cpp ${ENGINE_SRC_ROOT_DIR}/System/LoadSave/DemoReader.cpp ${ENGINE_SRC_ROOT_DIR}/System/LoadSave/DemoRecorder.cpp ${ENGINE_SRC_ROOT_DIR}/System/Log/Backend.cpp diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 099dc4dbe71..6c3592ed3a7 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -338,6 +338,23 @@ endif() add_spring_test(${test_name} "${test_src}" "${test_libs}" "-DTEST") +################################################################################ +### DemoFileExtension + set(test_name DemoFileExtension) + set(test_src + "${CMAKE_CURRENT_SOURCE_DIR}/engine/System/LoadSave/testDemoFileExtension.cpp" + "${ENGINE_SOURCE_DIR}/System/LoadSave/DemoFileExtension.cpp" + "${ENGINE_SOURCE_DIR}/System/Config/ConfigVariable.cpp" + "${ENGINE_SOURCE_DIR}/System/StringUtil.cpp" + ${test_Common_sources} + ) + + set(test_libs + z + ) + + add_spring_test(${test_name} "${test_src}" "${test_libs}" "") + if (NOT NO_CREG) ################################################################################ ### CREG diff --git a/test/engine/System/LoadSave/testDemoFileExtension.cpp b/test/engine/System/LoadSave/testDemoFileExtension.cpp new file mode 100644 index 00000000000..0788b113663 --- /dev/null +++ b/test/engine/System/LoadSave/testDemoFileExtension.cpp @@ -0,0 +1,205 @@ +/* This file is part of the Recoil engine (GPL v2 or later), see LICENSE.html */ + +#include +#include +#include +#include +#include + +#include + +#include + +#include "System/Config/ConfigHandler.h" +#include "System/LoadSave/DemoFileExtension.h" +#include "System/LoadSave/demofile.h" + +// Minimal ConfigHandler stub — just enough for GetDemoFileExtensions() +class StubConfigHandler : public ConfigHandler { +public: + std::map values; + + void FinalizeLoad() override {} + void SetString(const std::string&, const std::string&, bool, bool) override {} + std::string GetString(const std::string& key) const override + { + auto it = values.find(key); + return (it != values.end()) ? it->second : std::string{}; + } + bool IsSet(const std::string&) const override { return true; } + bool IsReadOnly(const std::string&) const override { return false; } + bool IsDeprecated(const std::string&) const override { return false; } + void Delete(const std::string&) override {} + std::string GetConfigFile() const override { return {}; } + const std::map GetData() const override { return {}; } + std::map GetDataWithoutDefaults() const override { return {}; } + void Update() override {} + void EnableWriting(bool) override {} +protected: + void AddObserver(ConfigNotifyCallback, void*, const std::vector&) override {} + void RemoveObserver(void*) override {} +}; + +ConfigHandler* configHandler = nullptr; + +struct TempGzFile { + std::string path; + + explicit TempGzFile(const void* data, int len) + { + path = std::tmpnam(nullptr); + gzFile f = gzopen(path.c_str(), "wb"); + REQUIRE(f != nullptr); + if (len > 0) + gzwrite(f, data, len); + gzclose(f); + } + + ~TempGzFile() { std::remove(path.c_str()); } + + TempGzFile(const TempGzFile&) = delete; + TempGzFile& operator=(const TempGzFile&) = delete; +}; + +struct DemoExtFixture { + StubConfigHandler stub; + + DemoExtFixture() { configHandler = &stub; } + ~DemoExtFixture() { configHandler = nullptr; } +}; + +// ============================= GetDemoFileExtensions ======================== + +TEST_CASE_METHOD(DemoExtFixture, "GetDemoFileExtensions: single default extension", "[DemoFileExtension]") +{ + stub.values["DemoFileExtension"] = "sdfz"; + auto exts = GetDemoFileExtensions(); + REQUIRE(exts.size() == 1); + CHECK(exts[0] == "sdfz"); +} + +TEST_CASE_METHOD(DemoExtFixture, "GetDemoFileExtensions: multiple extensions", "[DemoFileExtension]") +{ + stub.values["DemoFileExtension"] = "barreplay,sdfz"; + auto exts = GetDemoFileExtensions(); + REQUIRE(exts.size() == 2); + CHECK(exts[0] == "barreplay"); + CHECK(exts[1] == "sdfz"); +} + +TEST_CASE_METHOD(DemoExtFixture, "GetDemoFileExtensions: trims whitespace", "[DemoFileExtension]") +{ + stub.values["DemoFileExtension"] = " barreplay , sdfz "; + auto exts = GetDemoFileExtensions(); + REQUIRE(exts.size() == 2); + CHECK(exts[0] == "barreplay"); + CHECK(exts[1] == "sdfz"); +} + +TEST_CASE_METHOD(DemoExtFixture, "GetDemoFileExtensions: empty string falls back to sdfz", "[DemoFileExtension]") +{ + stub.values["DemoFileExtension"] = ""; + auto exts = GetDemoFileExtensions(); + REQUIRE(exts.size() == 1); + CHECK(exts[0] == "sdfz"); +} + +TEST_CASE_METHOD(DemoExtFixture, "GetDemoFileExtensions: skips empty segments", "[DemoFileExtension]") +{ + stub.values["DemoFileExtension"] = "foo,,bar"; + auto exts = GetDemoFileExtensions(); + REQUIRE(exts.size() == 2); + CHECK(exts[0] == "foo"); + CHECK(exts[1] == "bar"); +} + +TEST_CASE_METHOD(DemoExtFixture, "GetDemoFileExtensions: filters extensions with slash", "[DemoFileExtension]") +{ + stub.values["DemoFileExtension"] = "bad/ext,ok"; + auto exts = GetDemoFileExtensions(); + REQUIRE(exts.size() == 1); + CHECK(exts[0] == "ok"); +} + +TEST_CASE_METHOD(DemoExtFixture, "GetDemoFileExtensions: filters extensions with dot", "[DemoFileExtension]") +{ + stub.values["DemoFileExtension"] = "bad.ext,ok"; + auto exts = GetDemoFileExtensions(); + REQUIRE(exts.size() == 1); + CHECK(exts[0] == "ok"); +} + +TEST_CASE_METHOD(DemoExtFixture, "GetDemoFileExtensions: filters extensions with backslash", "[DemoFileExtension]") +{ + stub.values["DemoFileExtension"] = "bad\\ext,ok"; + auto exts = GetDemoFileExtensions(); + REQUIRE(exts.size() == 1); + CHECK(exts[0] == "ok"); +} + +TEST_CASE_METHOD(DemoExtFixture, "GetDemoFileExtensions: all invalid falls back to sdfz", "[DemoFileExtension]") +{ + stub.values["DemoFileExtension"] = "a/b,c.d,e\\f"; + auto exts = GetDemoFileExtensions(); + REQUIRE(exts.size() == 1); + CHECK(exts[0] == "sdfz"); +} + +// ============================= IsDemoExtension ============================= + +TEST_CASE_METHOD(DemoExtFixture, "IsDemoExtension: known extensions accepted", "[DemoFileExtension]") +{ + stub.values["DemoFileExtension"] = "barreplay,sdfz"; + CHECK(IsDemoExtension("sdfz")); + CHECK(IsDemoExtension("barreplay")); +} + +TEST_CASE_METHOD(DemoExtFixture, "IsDemoExtension: unknown extension rejected", "[DemoFileExtension]") +{ + stub.values["DemoFileExtension"] = "barreplay,sdfz"; + CHECK_FALSE(IsDemoExtension("mp4")); + CHECK_FALSE(IsDemoExtension("")); +} + +TEST_CASE_METHOD(DemoExtFixture, "IsDemoExtension: case insensitive", "[DemoFileExtension]") +{ + stub.values["DemoFileExtension"] = "sdfz"; + CHECK(IsDemoExtension("SDFZ")); + CHECK(IsDemoExtension("Sdfz")); + CHECK(IsDemoExtension("sdfz")); +} + +// ========================= ContentsLookLikeAReplay ========================= + +TEST_CASE("ContentsLookLikeAReplay: valid magic detected", "[DemoFileExtension]") +{ + char magic[16] = {}; + std::memcpy(magic, DEMOFILE_MAGIC, sizeof(magic)); + TempGzFile tmp(magic, sizeof(magic)); + CHECK(ContentsLookLikeAReplay(tmp.path)); +} + +TEST_CASE("ContentsLookLikeAReplay: wrong magic rejected", "[DemoFileExtension]") +{ + char magic[16] = "not a demofile!"; + TempGzFile tmp(magic, sizeof(magic)); + CHECK_FALSE(ContentsLookLikeAReplay(tmp.path)); +} + +TEST_CASE("ContentsLookLikeAReplay: too short content rejected", "[DemoFileExtension]") +{ + char data[5] = "spri"; + TempGzFile tmp(data, sizeof(data)); + CHECK_FALSE(ContentsLookLikeAReplay(tmp.path)); +} + +TEST_CASE("ContentsLookLikeAReplay: empty file rejected", "[DemoFileExtension]") +{ + TempGzFile tmp(nullptr, 0); + CHECK_FALSE(ContentsLookLikeAReplay(tmp.path)); +} + +TEST_CASE("ContentsLookLikeAReplay: nonexistent file rejected", "[DemoFileExtension]") +{ + CHECK_FALSE(ContentsLookLikeAReplay("/nonexistent/path/replay.sdfz")); +} From 50ce22be5d55452522ef4eaa78a8481f2d7d70a0 Mon Sep 17 00:00:00 2001 From: rlcevg <514675+rlcevg@users.noreply.github.com> Date: Tue, 28 Jul 2026 14:30:53 +0300 Subject: [PATCH 70/90] Update BARb 1.6.27 (#3130) --- AI/Skirmish/BARb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AI/Skirmish/BARb b/AI/Skirmish/BARb index 9875f1b332a..1d9952f3c79 160000 --- a/AI/Skirmish/BARb +++ b/AI/Skirmish/BARb @@ -1 +1 @@ -Subproject commit 9875f1b332aa037c7a9f6e8bea1f8ecd9f289096 +Subproject commit 1d9952f3c79d2bba3b30cf7c00403ddacf3b6723 From 22dd3145ca67e82585a08f9570c3aaee154a69a0 Mon Sep 17 00:00:00 2001 From: bruno-dasilva <8520801+bruno-dasilva@users.noreply.github.com> Date: Tue, 28 Jul 2026 04:40:30 -0700 Subject: [PATCH 71/90] fix: link test_DemoFileExtension against ZLIB::ZLIB for windows build (#3139) * Fix Windows build: link test_DemoFileExtension against ZLIB::ZLIB The DemoFileExtension test target linked the bare library name `z`, which expands to `-lz`. That works on Linux where zlib sits in the default library search path, but the mingw cross-linker fails with "cannot find -lz" because on Windows zlib is supplied by mingwlibs64. Use the ZLIB::ZLIB imported target instead, matching how the engine, unitsync and DemoTool link zlib. find_package is needed here because imported targets are directory-scoped and test/ is a sibling of rts/. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01FuDx3RoNDdSDMTx7Nkw8ss * Apply suggestion from @sprunk Co-authored-by: sprunk --------- Co-authored-by: Claude Co-authored-by: TarnishedKnight Co-authored-by: sprunk --- test/CMakeLists.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 6c3592ed3a7..12061d12afc 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -349,8 +349,10 @@ endif() ${test_Common_sources} ) + find_package_static(ZLIB 1.2.7 REQUIRED) + set(test_libs - z + ZLIB::ZLIB ) add_spring_test(${test_name} "${test_src}" "${test_libs}" "") From 77e9065fbee0074f76d8c5555eb780cdb4eb1b86 Mon Sep 17 00:00:00 2001 From: lostsquirrel1 Date: Tue, 28 Jul 2026 12:41:10 +0100 Subject: [PATCH 72/90] Updated changelog --- doc/site/content/changelogs/_index.markdown | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/site/content/changelogs/_index.markdown b/doc/site/content/changelogs/_index.markdown index dfbde9307f9..1e8e738da3a 100644 --- a/doc/site/content/changelogs/_index.markdown +++ b/doc/site/content/changelogs/_index.markdown @@ -8,6 +8,7 @@ title = "Running changelog" This is the bleeding-edge changelog since version 2026.06, for **pre-release 2026.07**. ## Caveats +- Barbarian AI updated to version 1.6.26 ## Build - Fixes for MSVC build @@ -78,6 +79,7 @@ This is the bleeding-edge changelog since version 2026.06, for **pre-release 202 - Fix /unbind for keychains - Fix stale spGetActionHotKeys returns [PR 3082](https://github.com/beyond-all-reason/RecoilEngine/pull/3082) - Use a portable type cast in MemPoolTypes logging +- Support alternate file extensions for replays [PR 2975](https://github.com/beyond-all-reason/RecoilEngine/pull/2975) ## Rendering - Added sorting icon names before adding to atlas so insertion order is consistent across runs. From 7104ab1f76d9fe6823016dcb5561e179174e39a2 Mon Sep 17 00:00:00 2001 From: Tom J Nowell Date: Tue, 28 Jul 2026 13:55:19 +0100 Subject: [PATCH 73/90] Sim/COB: avoid UB in float-to-short angle casts (fixes arm64/x86 desync) (#3075) * Sim/COB: avoid UB in float-to-short angle casts (fixes arm64/x86 desync) Casting a floating-point heading/pitch expression directly to `short` is undefined behaviour when the truncated value does not fit in `short`, and this is genuinely reached: `heading * RAD2TAANG` equals +-32768 at heading = +-pi, one past short's 32767 maximum. Because the result is UB, arm64 and x86 produced different values at the angle extremes, which was observed as an arm64/x86 multiplayer desync. x86 already lowers `short(float)` as float->int->short (cvttss2si into a 32-bit register, then narrow), so making the `int()` step explicit leaves the already-correct x86 result unchanged while pinning arm64 (whose fcvtzs saturated differently) to the same value. The int->short narrowing is the intended 16-bit TA-angle wraparound. Sites fixed in CobInstance.cpp: WindChanged, StartBuilding, AimWeapon. Sibling casts were checked and left untouched: UnitScript.cpp:1058 casts asin(...)*RAD2TAANG (range +-16384, always fits short, no UB) and lines 1095/1099 already route through int; CobThread.cpp has no such casts. Pure correctness fix to synced simulation code; it does not alter results on the platform that was already correct. Cross-arch sync validation is recommended. Origin: discussed in PR #2991's review (credit BambaDamba). AI assistance: implemented with Claude Code (Anthropic) from a written plan; reasoning and build verification reviewed by a human. * address review: extract RadAngleToCobShort helper with explanatory comment sprunk asked for a comment and a named helper around the float->short angle cast. Wrap the conversion in RadAngleToCobShort() and document why it exists: COB angles are circular 16-bit TA units (full turn == COBSCALE), so values past a half turn intentionally wrap modulo 2^16 via the int->short narrowing. The float->int step stays because the wrap is the desired behaviour and is deterministic across arm64/x86; clamping the range (as suggested) would break angles past a half turn rather than wrapping them. No behavioural change vs the previous short(int(...)) form. --- rts/Sim/Units/Scripts/CobInstance.cpp | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/rts/Sim/Units/Scripts/CobInstance.cpp b/rts/Sim/Units/Scripts/CobInstance.cpp index f4028a80797..e073959df9b 100644 --- a/rts/Sim/Units/Scripts/CobInstance.cpp +++ b/rts/Sim/Units/Scripts/CobInstance.cpp @@ -42,6 +42,20 @@ /******************************************************************************/ /******************************************************************************/ +// COB scripts encode angles as TA units where a full turn is COBSCALE (65536), +// so any angle past a half turn exceeds the range of a signed short and is meant +// to wrap around (it is a circular 16-bit angle). Truncating the scaled value to +// int first is well defined for the bounded angles the sim feeds in, and the +// following int->short narrowing performs that modular wrap deterministically. +// Converting straight from float to short would be undefined behaviour once the +// value leaves short's range, and produced different results on arm64 vs x86, +// desyncing multiplayer. +static inline short RadAngleToCobShort(float radAngle) +{ + return static_cast(static_cast(radAngle * RAD2TAANG)); +} + + CR_BIND_DERIVED(CCobInstance, CUnitScript, ) CR_REG_METADATA(CCobInstance, ( @@ -237,7 +251,7 @@ void CCobInstance::WindChanged(float heading, float speed) { ZoneScoped; Call(COBFN_SetSpeed, int(speed * 3000.0f)); - Call(COBFN_SetDirection, short(heading * RAD2TAANG)); + Call(COBFN_SetDirection, RadAngleToCobShort(heading)); } @@ -387,8 +401,8 @@ void CCobInstance::StartBuilding(float heading, float pitch) std::array callinArgs; callinArgs[0] = 2; - callinArgs[1] = short(heading * RAD2TAANG); - callinArgs[2] = short( pitch * RAD2TAANG); + callinArgs[1] = RadAngleToCobShort(heading); + callinArgs[2] = RadAngleToCobShort(pitch); Call(COBFN_StartBuilding, callinArgs); } @@ -439,8 +453,8 @@ void CCobInstance::AimWeapon(int weaponNum, float heading, float pitch) std::array callinArgs; callinArgs[0] = 2; - callinArgs[1] = short(heading * RAD2TAANG); - callinArgs[2] = short( pitch * RAD2TAANG); + callinArgs[1] = RadAngleToCobShort(heading); + callinArgs[2] = RadAngleToCobShort(pitch); Call(COBFN_AimPrimary + COBFN_Weapon_Funcs * weaponNum, callinArgs, CBAimWeapon, weaponNum, nullptr); } From 44ff15f2dffc25a55b99b175b7d3dbf215eb3180 Mon Sep 17 00:00:00 2001 From: Alexander Heistermann Date: Tue, 28 Jul 2026 19:37:14 -0500 Subject: [PATCH 74/90] Fail loading when modrules parsing fails --- rts/Sim/Misc/ModInfo.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rts/Sim/Misc/ModInfo.cpp b/rts/Sim/Misc/ModInfo.cpp index 39e9f183c0d..82ca63b5256 100644 --- a/rts/Sim/Misc/ModInfo.cpp +++ b/rts/Sim/Misc/ModInfo.cpp @@ -180,7 +180,7 @@ void CModInfo::Init(const std::string& modFileName) parser.Execute(); if (!parser.IsValid()) - LOG_L(L_ERROR, "[ModInfo::%s] error \"%s\" loading mod-rules, using defaults", __func__, parser.GetErrorLog().c_str()); + throw content_error(fmt::format("Failed to load gamedata/modrules.lua: {}", parser.GetErrorLog())); const LuaTable& root = parser.GetRoot(); From 1435ef89d7396a57c7fdaf0727377e492d66b99f Mon Sep 17 00:00:00 2001 From: bruno-dasilva Date: Wed, 29 Jul 2026 02:25:56 -0700 Subject: [PATCH 75/90] refactor: extract ghost drawing to DrawGhostedBuildings (#3107) Ghost drawing in GLSL was inline in DrawAlphaUnit. Ghost drawing in GL4 was further inlined into DrawAlphaObjects. Both needed flags to pass around which makes the whole thing kinda awkward. This was ok in current world but the follow up PRs add more params so it's best to split them ahead of time. We pull the ghost drawing impl out into its own DrawGhostedBuildings function in both paths, then call them from DrawAlphaObjects. Simplifies the code and also makes the GLSL/GL4 paths more symmetric. Pure refactor, no behavior change. Co-authored-by: Bruno Da Silva --- rts/Rendering/Units/UnitDrawer.cpp | 115 +++++++++++++++-------------- rts/Rendering/Units/UnitDrawer.h | 4 +- 2 files changed, 61 insertions(+), 58 deletions(-) diff --git a/rts/Rendering/Units/UnitDrawer.cpp b/rts/Rendering/Units/UnitDrawer.cpp index a439038dabe..346cdff97d9 100644 --- a/rts/Rendering/Units/UnitDrawer.cpp +++ b/rts/Rendering/Units/UnitDrawer.cpp @@ -822,7 +822,7 @@ void CUnitDrawerGLSL::DrawAlphaObjects(int modelType, bool drawReflection, bool CModelDrawerHelper::BindModelTypeTexture(modelType, mdlRenderer.GetObjectBinKey(i)); for (auto* o : mdlRenderer.GetObjectBin(i)) { - DrawAlphaUnit(o, modelType, thisPassMask, false); + DrawAlphaUnit(o, thisPassMask); } } @@ -869,66 +869,33 @@ void CUnitDrawerGLSL::DrawGhostedBuildings(int modelType) const glColor4f(0.6f, 0.6f, 0.6f, IModelDrawerState::alphaValues.y); // buildings that died while ghosted - for (GhostSolidObject* dgb : deadGhostedBuildings) { - if (camera->InView(dgb->pos, dgb->GetModel()->GetDrawRadius())) { - glPushMatrix(); - glTranslatef3(dgb->pos); - glRotatef(dgb->facing * 90.0f, 0, 1, 0); + for (const GhostSolidObject* dgb : deadGhostedBuildings) { + const S3DModel* model = dgb->GetModel(); + if (!camera->InView(dgb->pos, model->GetDrawRadius())) + continue; - CModelDrawerHelper::BindModelTypeTexture(modelType, dgb->GetModel()->textureType); - SetTeamColor(dgb->team, IModelDrawerState::alphaValues.y); + glPushMatrix(); + glTranslatef3(dgb->pos); + glRotatef(dgb->facing * 90.0f, 0, 1, 0); - dgb->GetModel()->DrawStatic(); - glPopMatrix(); - } - } + CModelDrawerHelper::BindModelTypeTexture(modelType, model->textureType); + SetTeamColor(dgb->team, IModelDrawerState::alphaValues.y); - for (CUnit* lgb : liveGhostedBuildings) { - DrawAlphaUnit(lgb, modelType, DrawFlags::SO_ALPHAF_FLAG, true); + model->DrawStatic(); + glPopMatrix(); } -} - -void CUnitDrawerGLSL::DrawOpaqueUnit(CUnit* unit, uint8_t thisPassMask) const -{ - RECOIL_DETAILED_TRACY_ZONE; - if (!ShouldDrawOpaqueUnit(unit, thisPassMask)) - return; - - // draw the unit with the default (non-Lua) material - SetTeamColor(unit->team); - DrawUnitTrans(unit, 0, 0, false, false); -} -void CUnitDrawerGLSL::DrawUnitShadow(CUnit* unit) const -{ - RECOIL_DETAILED_TRACY_ZONE; - if (ShouldDrawUnitShadow(unit)) - DrawUnitTrans(unit, 0, 0, false, false); -} - -void CUnitDrawerGLSL::DrawAlphaUnit(CUnit* unit, int modelType, uint8_t thisPassMask, bool drawGhostBuildingsPass) const -{ - RECOIL_DETAILED_TRACY_ZONE; - if (!drawGhostBuildingsPass && !ShouldDrawAlphaUnit(unit, thisPassMask)) - return; - - const unsigned short losStatus = unit->losStatus[gu->myAllyTeam]; - - if (drawGhostBuildingsPass) { + // buildings that left LOS but are still alive + for (CUnit* unit : liveGhostedBuildings) { // check for decoy models const UnitDef* decoyDef = unit->unitDef->decoyDef; - const S3DModel* model = nullptr; - - if (decoyDef == nullptr) { - model = unit->model; - } - else { - model = decoyDef->LoadModel(); - } + const S3DModel* model = (decoyDef == nullptr) ? unit->model : decoyDef->LoadModel(); // FIXME: needs a second pass if (model->type != modelType) - return; + continue; + + const unsigned short losStatus = unit->losStatus[gu->myAllyTeam]; // ghosted enemy units if (losStatus & LOS_CONTRADAR) { @@ -948,17 +915,44 @@ void CUnitDrawerGLSL::DrawAlphaUnit(CUnit* unit, int modelType, uint8_t thisPass // not actually cloaked CModelDrawerHelper::BindModelTypeTexture(modelType, model->textureType); - SetTeamColor(unit->team, (losStatus & LOS_CONTRADAR) ? IModelDrawerState::alphaValues.z : IModelDrawerState::alphaValues.y); + const float ghostAlpha = (losStatus & LOS_CONTRADAR) ? IModelDrawerState::alphaValues.z : IModelDrawerState::alphaValues.y; + SetTeamColor(unit->team, ghostAlpha); model->DrawStatic(); glPopMatrix(); glColor4f(1.0f, 1.0f, 1.0f, IModelDrawerState::alphaValues.x); - return; } +} + +void CUnitDrawerGLSL::DrawOpaqueUnit(CUnit* unit, uint8_t thisPassMask) const +{ + RECOIL_DETAILED_TRACY_ZONE; + if (!ShouldDrawOpaqueUnit(unit, thisPassMask)) + return; + + // draw the unit with the default (non-Lua) material + SetTeamColor(unit->team); + DrawUnitTrans(unit, 0, 0, false, false); +} + +void CUnitDrawerGLSL::DrawUnitShadow(CUnit* unit) const +{ + RECOIL_DETAILED_TRACY_ZONE; + if (ShouldDrawUnitShadow(unit)) + DrawUnitTrans(unit, 0, 0, false, false); +} + +void CUnitDrawerGLSL::DrawAlphaUnit(CUnit* unit, uint8_t thisPassMask) const +{ + RECOIL_DETAILED_TRACY_ZONE; + if (!ShouldDrawAlphaUnit(unit, thisPassMask)) + return; if (unit->GetIsIcon()) return; + const unsigned short losStatus = unit->losStatus[gu->myAllyTeam]; + if ((losStatus & LOS_INLOS) || gu->spectatingFullView) { SetTeamColor(unit->team, IModelDrawerState::alphaValues.x); DrawUnitTrans(unit, 0, 0, false, false); @@ -1738,12 +1732,21 @@ void CUnitDrawerGL4::DrawAlphaObjects(int modelType, bool drawReflection, bool d smv.Submit(GL_TRIANGLES, false); } - // void CGLUnitDrawer::DrawGhostedBuildings(int modelType) - if (gu->spectatingFullView) - return; + smv.Unbind(); + // living and dead ghosted buildings + if (!gu->spectatingFullView) + DrawGhostedBuildings(modelType); +} + +void CUnitDrawerGL4::DrawGhostedBuildings(int modelType) const +{ + RECOIL_DETAILED_TRACY_ZONE; const auto& deadGhostBuildings = modelDrawerData->GetDeadGhostBuildings(gu->myAllyTeam, modelType); + auto& smv = S3DModelVAO::GetInstance(); + smv.Bind(); + const auto oldMM = modelDrawerState->SetMatrixMode(ShaderMatrixModes::STATIC_MATMODE); // deadGhostedBuildings { diff --git a/rts/Rendering/Units/UnitDrawer.h b/rts/Rendering/Units/UnitDrawer.h index d6c6935afc6..97106632f76 100644 --- a/rts/Rendering/Units/UnitDrawer.h +++ b/rts/Rendering/Units/UnitDrawer.h @@ -167,7 +167,7 @@ class CUnitDrawerGLSL : public CUnitDrawerBase { void DrawOpaqueUnit(CUnit* unit, uint8_t thisPassMask) const; void DrawUnitShadow(CUnit* unit) const; - void DrawAlphaUnit(CUnit* unit, int modelType, uint8_t thisPassMask, bool drawGhostBuildingsPass) const; + void DrawAlphaUnit(CUnit* unit, uint8_t thisPassMask) const; void DrawOpaqueAIUnit(const CUnitDrawerData::TempDrawUnit& unit) const; void DrawAlphaAIUnit(const CUnitDrawerData::TempDrawUnit& unit) const; @@ -236,7 +236,7 @@ class CUnitDrawerGL4 final : public CUnitDrawerGLSL { void DrawOpaqueObjectsAux(int modelType) const override; void DrawOpaqueAIUnit(const CUnitDrawerData::TempDrawUnit& unit) const; - void DrawGhostedBuildings(int modelType) const override {} //implemented in-line + void DrawGhostedBuildings(int modelType) const override; void DrawUnitModelBeingBuiltShadow(const CUnit* unit, bool noLuaCall) const; void DrawUnitModelBeingBuiltOpaque(const CUnit* unit, bool noLuaCall) const; From eab16157c723715bdd42e46d288b5cd8cb1e6472 Mon Sep 17 00:00:00 2001 From: bruno-dasilva Date: Wed, 29 Jul 2026 02:25:56 -0700 Subject: [PATCH 76/90] fix(GLSL/GL4): draw ghost buildings with the palette they were last seen under (#3108) This fixes two bugs: 1. recently we added custom color palette functionality, but that was not extended to ghosts (so they are currently using just the team color). 2. Live ghosts read the color from the live CUnit at draw time, so if a unit changes team while in fog of war (eg. unit gifts), that information would be leaked. So, to fix, we now a) use paletteIndex for ghost color and b) snapshot the last seen paletteIndex for that unit. Co-authored-by: Bruno Da Silva --- rts/Rendering/Units/UnitDrawer.cpp | 41 +++++++++++++------------- rts/Rendering/Units/UnitDrawerData.cpp | 26 +++++++++++++--- rts/Rendering/Units/UnitDrawerData.h | 18 ++++++++++- 3 files changed, 59 insertions(+), 26 deletions(-) diff --git a/rts/Rendering/Units/UnitDrawer.cpp b/rts/Rendering/Units/UnitDrawer.cpp index 346cdff97d9..2ff97165935 100644 --- a/rts/Rendering/Units/UnitDrawer.cpp +++ b/rts/Rendering/Units/UnitDrawer.cpp @@ -886,7 +886,9 @@ void CUnitDrawerGLSL::DrawGhostedBuildings(int modelType) const } // buildings that left LOS but are still alive - for (CUnit* unit : liveGhostedBuildings) { + for (const auto& lgb : liveGhostedBuildings) { + const CUnit* unit = lgb.unit; + // check for decoy models const UnitDef* decoyDef = unit->unitDef->decoyDef; const S3DModel* model = (decoyDef == nullptr) ? unit->model : decoyDef->LoadModel(); @@ -915,8 +917,9 @@ void CUnitDrawerGLSL::DrawGhostedBuildings(int modelType) const // not actually cloaked CModelDrawerHelper::BindModelTypeTexture(modelType, model->textureType); + // color with the team the unit was last seen under, not the live unit's current team const float ghostAlpha = (losStatus & LOS_CONTRADAR) ? IModelDrawerState::alphaValues.z : IModelDrawerState::alphaValues.y; - SetTeamColor(unit->team, ghostAlpha); + SetTeamColor(lgb.team, ghostAlpha); model->DrawStatic(); glPopMatrix(); @@ -1772,7 +1775,7 @@ void CUnitDrawerGL4::DrawGhostedBuildings(int modelType) const } modelDrawerState->SetStaticModelMatrix(staticWorldMat); - smv.SubmitImmediately(dgb->GetModel(), static_cast(dgb->team)); //need to submit immediately every model because of static per-model matrix + smv.SubmitImmediately(dgb->GetModel(), dgb->paletteIndex); //need to submit immediately every model because of static per-model matrix } } @@ -1782,20 +1785,14 @@ void CUnitDrawerGL4::DrawGhostedBuildings(int modelType) const int prevModelType = -1; int prevTexType = -1; - for (const auto* lgb : liveGhostedBuildings) { - if (!camera->InView(lgb->pos, lgb->model->GetDrawRadius())) + for (const auto& lgb : liveGhostedBuildings) { + const CUnit* u = lgb.unit; + if (!camera->InView(u->pos, u->model->GetDrawRadius())) continue; // check for decoy models - const UnitDef* decoyDef = lgb->unitDef->decoyDef; - const S3DModel* model = nullptr; - - if (decoyDef == nullptr) { - model = lgb->model; - } - else { - model = decoyDef->LoadModel(); - } + const UnitDef* decoyDef = u->unitDef->decoyDef; + const S3DModel* model = (decoyDef == nullptr) ? u->model : decoyDef->LoadModel(); // FIXME: needs a second pass if (model->type != modelType) @@ -1804,20 +1801,21 @@ void CUnitDrawerGL4::DrawGhostedBuildings(int modelType) const static CMatrix44f staticWorldMat; staticWorldMat.LoadIdentity(); - staticWorldMat.Translate(lgb->pos); + staticWorldMat.Translate(u->pos); - staticWorldMat.RotateY(-lgb->buildFacing * math::DEG_TO_RAD * 90.0f); + staticWorldMat.RotateY(-u->buildFacing * math::DEG_TO_RAD * 90.0f); - const unsigned short losStatus = lgb->losStatus[gu->myAllyTeam]; + const unsigned short losStatus = u->losStatus[gu->myAllyTeam]; - // ghosted enemy units + // ghosted enemy units; SetTeamColor only gates the alpha here (the per-instance + // paletteIndex passed to SubmitImmediately drives the actual color) if (losStatus & LOS_CONTRADAR) { modelDrawerState->SetColorMultiplier(0.9f, 0.9f, 0.9f, IModelDrawerState::alphaValues.z); - modelDrawerState->SetTeamColor(lgb->team, IModelDrawerState::alphaValues.z); + modelDrawerState->SetTeamColor(lgb.team, IModelDrawerState::alphaValues.z); } else { modelDrawerState->SetColorMultiplier(0.6f, 0.6f, 0.6f, IModelDrawerState::alphaValues.y); - modelDrawerState->SetTeamColor(lgb->team, IModelDrawerState::alphaValues.y); + modelDrawerState->SetTeamColor(lgb.team, IModelDrawerState::alphaValues.y); } if (prevModelType != modelType || prevTexType != model->textureType) { @@ -1826,7 +1824,8 @@ void CUnitDrawerGL4::DrawGhostedBuildings(int modelType) const } modelDrawerState->SetStaticModelMatrix(staticWorldMat); - smv.SubmitImmediately(model, static_cast(lgb->team)); //need to submit immediately every model because of static per-model matrix + // draw with the palette the unit was last seen under, not the live unit's current one + smv.SubmitImmediately(model, lgb.paletteIndex); //need to submit immediately every model because of static per-model matrix } } diff --git a/rts/Rendering/Units/UnitDrawerData.cpp b/rts/Rendering/Units/UnitDrawerData.cpp index 3f98205280a..8059312f907 100644 --- a/rts/Rendering/Units/UnitDrawerData.cpp +++ b/rts/Rendering/Units/UnitDrawerData.cpp @@ -48,6 +48,7 @@ CR_REG_METADATA(GhostSolidObject, ( CR_MEMBER(facing), CR_MEMBER(team), + CR_MEMBER(paletteIndex), CR_IGNORED(currentIconIndex), CR_IGNORED(model), @@ -55,6 +56,13 @@ CR_REG_METADATA(GhostSolidObject, ( CR_POSTLOAD(PostLoad) )) +CR_BIND(CUnitDrawerData::LiveGhostBuilding, ) +CR_REG_METADATA(CUnitDrawerData::LiveGhostBuilding, ( + CR_MEMBER(unit), + CR_MEMBER(paletteIndex), + CR_MEMBER(team) +)) + CR_BIND(CUnitDrawerData::TempDrawUnit, ) CR_REG_METADATA(CUnitDrawerData::TempDrawUnit, ( CR_MEMBER(unitDefId), @@ -617,6 +625,7 @@ bool CUnitDrawerData::UpdateUnitGhosts(const CUnit* unit, const bool addNewGhost gso->facing = u->buildFacing; gso->dir = u->frontdir; gso->team = u->team; + gso->paletteIndex = u->paletteIndex; gso->radius = u->radius; gso->GetModel(); @@ -640,7 +649,8 @@ bool CUnitDrawerData::UpdateUnitGhosts(const CUnit* unit, const bool addNewGhost } - spring::VectorErase(savedData.liveGhostBuildings[allyTeam][MDL_TYPE(u)], u); + spring::VectorEraseIf(savedData.liveGhostBuildings[allyTeam][MDL_TYPE(u)], + [u](const LiveGhostBuilding& lgb) { return lgb.unit == u; }); } return addedOwnAllyTeam; } @@ -674,7 +684,8 @@ void CUnitDrawerData::UnitEnteredLos(const CUnit* unit, int allyTeam) CUnit* u = const_cast(unit); //cleanup if (unit->leavesGhost) - spring::VectorErase(savedData.liveGhostBuildings[allyTeam][MDL_TYPE(unit)], u); + spring::VectorEraseIf(savedData.liveGhostBuildings[allyTeam][MDL_TYPE(unit)], + [u](const LiveGhostBuilding& lgb) { return lgb.unit == u; }); if (allyTeam != gu->myAllyTeam) return; @@ -687,8 +698,15 @@ void CUnitDrawerData::UnitLeftLos(const CUnit* unit, int allyTeam) RECOIL_DETAILED_TRACY_ZONE; CUnit* u = const_cast(unit); //cleanup - if (unit->leavesGhost) - spring::VectorInsertUnique(savedData.liveGhostBuildings[allyTeam][MDL_TYPE(unit)], u, true); + if (unit->leavesGhost) { + // snapshot the color the unit is last seen under so a later team change (while out of LOS) + // does not recolor its ghost. keep the earliest snapshot if it re-fires without re-entering. + auto& lgbs = savedData.liveGhostBuildings[allyTeam][MDL_TYPE(unit)]; + const bool alreadyGhosted = std::any_of(lgbs.begin(), lgbs.end(), + [u](const LiveGhostBuilding& lgb) { return lgb.unit == u; }); + if (!alreadyGhosted) + lgbs.push_back({ u, u->paletteIndex, static_cast(u->team) }); + } if (allyTeam != gu->myAllyTeam) return; diff --git a/rts/Rendering/Units/UnitDrawerData.h b/rts/Rendering/Units/UnitDrawerData.h index 36630545555..a2debe2b7bb 100644 --- a/rts/Rendering/Units/UnitDrawerData.h +++ b/rts/Rendering/Units/UnitDrawerData.h @@ -34,7 +34,13 @@ class GhostSolidObject { int refCount; int facing; //FIXME replaced with dir-vector just legacy decal drawer uses this + + // color identity captured when the ghost was created; a ghost keeps the color it was last + // seen under. team drives the legacy (GLSL) team-color path, paletteIndex drives the GL4 + // per-instance palette (equal to team unless a custom Lua color palette was assigned). uint8_t team; + uint16_t paletteIndex; + size_t currentIconIndex; private: mutable const S3DModel* model; @@ -86,6 +92,16 @@ class CUnitDrawerData : public CUnitDrawerDataBase { private: mutable const UnitDef* unitDef; }; + // a still-alive building that left an observer's LOS. The unit is drawn as a ghost using the + // color it was last seen under (snapshotted here), so it does not silently recolor if the live + // unit changes team while out of LOS. team feeds the legacy (GLSL) team-color path, paletteIndex + // feeds the GL4 per-instance palette (equal to team unless a custom Lua palette was assigned). + struct LiveGhostBuilding { + CR_DECLARE_STRUCT(LiveGhostBuilding) + CUnit* unit = nullptr; + uint16_t paletteIndex = 0; + uint8_t team = 0; + }; struct SavedData { CR_DECLARE_STRUCT(SavedData) @@ -97,7 +113,7 @@ class CUnitDrawerData : public CUnitDrawerDataBase { std::vector, MODELTYPE_CNT>> deadGhostBuildings; /// buildings that left LOS but are still alive - std::vector, MODELTYPE_CNT>> liveGhostBuildings; + std::vector, MODELTYPE_CNT>> liveGhostBuildings; }; public: CUnitDrawerData(bool& mtModelDrawer_); From 223f9245f9ecaff7ce73cdbcf4cb66c50c9fab2c Mon Sep 17 00:00:00 2001 From: bruno-dasilva Date: Wed, 29 Jul 2026 02:25:57 -0700 Subject: [PATCH 77/90] refactor(GL4): extract EmplaceInstance submission primitive (#3109) Extract the build + queue impl out of AddToSubmissionImpl into a private EmplaceInstance(), so in the next PR we can add another caller to EmplaceInstance(). Results in cleaner code without passing around a bunch of flags in the follow ups. Behavior should be unchanged. Co-authored-by: Bruno Da Silva --- rts/Rendering/Models/3DModelVAO.cpp | 31 +++++++++++++++++++---------- rts/Rendering/Models/3DModelVAO.hpp | 11 ++++++++++ 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/rts/Rendering/Models/3DModelVAO.cpp b/rts/Rendering/Models/3DModelVAO.cpp index 7be3e00733f..33025974957 100644 --- a/rts/Rendering/Models/3DModelVAO.cpp +++ b/rts/Rendering/Models/3DModelVAO.cpp @@ -286,14 +286,28 @@ void S3DModelVAO::DrawElements(GLenum prim, uint32_t vboIndxStart, uint32_t vboI glDrawElements(prim, vboIndxCount, GL_UNSIGNED_INT, indxVBO.GetPtr(vboIndxStart * sizeof(uint32_t))); } +bool S3DModelVAO::EmplaceInstance(uint32_t indexStart, uint32_t indexCount, uint32_t traIndex, uint16_t paletteIndex, uint16_t numPieces, uint32_t uniIndex, uint32_t bposeIndex) +{ + RECOIL_DETAILED_TRACY_ZONE; + if (traIndex == TransformsMemStorage::INVALID_INDEX || bposeIndex == TransformsMemStorage::INVALID_INDEX) + return false; + + modelDataToInstance[SIndexAndCount{ indexStart, indexCount }].emplace_back(SInstanceData( + traIndex, + paletteIndex, + numPieces, + uniIndex, + bposeIndex + )); + + return true; +} + template bool S3DModelVAO::AddToSubmissionImpl(const TObj* obj, uint32_t indexStart, uint32_t indexCount, uint16_t paletteIndex) { RECOIL_DETAILED_TRACY_ZONE; const auto traIndex = transformsUploader.GetElemOffset(obj); - if (traIndex == TransformsMemStorage::INVALID_INDEX) - return false; - const auto uniIndex = modelUniformsStorage.GetObjOffset(obj); //doesn't need to exist for defs and models. Don't check for validity uint16_t numPieces = 0; @@ -307,19 +321,14 @@ bool S3DModelVAO::AddToSubmissionImpl(const TObj* obj, uint32_t indexStart, uint bposeIndex = transformsUploader.GetElemOffset(obj->model); } - if (bposeIndex == TransformsMemStorage::INVALID_INDEX) - return false; - - auto& modelInstanceData = modelDataToInstance[SIndexAndCount{ indexStart, indexCount }]; - modelInstanceData.emplace_back(SInstanceData( + return EmplaceInstance( + indexStart, indexCount, static_cast(traIndex), paletteIndex, numPieces, static_cast(uniIndex), static_cast(bposeIndex) - )); - - return true; + ); } bool S3DModelVAO::AddToSubmission(const S3DModel* model, uint16_t paletteIndex) diff --git a/rts/Rendering/Models/3DModelVAO.hpp b/rts/Rendering/Models/3DModelVAO.hpp index 4a5a6f359d1..d9405818f46 100644 --- a/rts/Rendering/Models/3DModelVAO.hpp +++ b/rts/Rendering/Models/3DModelVAO.hpp @@ -102,6 +102,17 @@ class S3DModelVAO { uint32_t indexCount, uint16_t paletteIndex ); + // build one SInstanceData from already-resolved offsets and queue it for the next Submit(); + // returns false (drawing nothing) if the world transform or bind pose is unavailable. + bool EmplaceInstance( + uint32_t indexStart, + uint32_t indexCount, + uint32_t traIndex, + uint16_t paletteIndex, + uint16_t numPieces, + uint32_t uniIndex, + uint32_t bposeIndex + ); void EnableAttribs(bool inst) const; void DisableAttribs() const; private: From 1dfbd72d7bef212fef740fdc1dc5cda8eb7157e9 Mon Sep 17 00:00:00 2001 From: bruno-dasilva Date: Wed, 29 Jul 2026 02:25:57 -0700 Subject: [PATCH 78/90] perf(GL4): batch ghost-building rendering (#3110) * refactor(GL4): replace matrix modes magic number with an enum Add SetFlag("MATMODE_XXX", ...) alongside the existing GBUFFER_*_IDX flags so the GL4 model shaders can reference the matrix-mode enum value by name instead of hardcoding a literal. Intended as a precursor for the ghost draw perf fixes, which add a third mode in the shader. * refactor(GL4): add ARRAY_MATMODE static-instanced model draw path * perf(GL4): batch ghost-building rendering via ARRAY_MATMODE Ghost buildings are static (no animation, never move), so each gets a single world-transform slot in the transforms SSBO, and is drawn batched through ARRAY_MATMODE. One multidraw per (color bucket x texture type) instead of one immediate draw per ghost. --------- Co-authored-by: Bruno Da Silva --- .../shaders/GLSL/ModelVertProgGL4.glsl | 19 ++- rts/Rendering/Common/ModelDrawerState.cpp | 4 + rts/Rendering/Models/3DModelVAO.cpp | 16 +++ rts/Rendering/Models/3DModelVAO.hpp | 3 + rts/Rendering/Units/UnitDrawer.cpp | 122 +++++++++++------- rts/Rendering/Units/UnitDrawerData.cpp | 61 +++++++++ rts/Rendering/Units/UnitDrawerData.h | 17 +++ 7 files changed, 187 insertions(+), 55 deletions(-) diff --git a/cont/base/springcontent/shaders/GLSL/ModelVertProgGL4.glsl b/cont/base/springcontent/shaders/GLSL/ModelVertProgGL4.glsl index 468d850874e..aa05d764667 100644 --- a/cont/base/springcontent/shaders/GLSL/ModelVertProgGL4.glsl +++ b/cont/base/springcontent/shaders/GLSL/ModelVertProgGL4.glsl @@ -262,7 +262,7 @@ Transform Lerp(Transform t0, Transform t1, float a) { void GetModelSpaceVertex(out vec4 msPosition, out vec3 msNormal) { - bool staticModel = (matrixMode > 0); + bool staticModel = (matrixMode == MATMODE_STATIC || matrixMode == MATMODE_ARRAY); vec4 piecePos = vec4(pos, 1.0); vec4 normal4 = vec4(normal, 0.0); @@ -271,7 +271,10 @@ void GetModelSpaceVertex(out vec4 msPosition, out vec3 msNormal) Transform tx; if (staticModel) { - tx = transforms[instData.x + bID0]; + // pieces always come from the bind-pose block (instData.w). In ARRAY_MATMODE + // instData.x is the per-instance world transform, not the bind pose; for static + // model submits instData.x == instData.w anyway, so instData.w is correct for both. + tx = transforms[instData.w + bID0]; } else { // do interpolation tx = Lerp( @@ -327,16 +330,20 @@ void GetModelSpaceVertex(out vec4 msPosition, out vec3 msNormal) void main(void) { - bool staticModel = (matrixMode > 0); - vec4 modelPos; vec3 modelNormal; GetModelSpaceVertex(modelPos, modelNormal); - if (staticModel) { + if (matrixMode == MATMODE_ARRAY) { + // static instanced: per-instance world transform read from the SSBO (no interpolation) + Transform wtx = transforms[instData.x + 0u]; + worldPos = ApplyTransform(wtx, modelPos); + wtx.trSc = vec4(0, 0, 0, 1); //nullify the translation part for the normal + worldNormal = ApplyTransform(wtx, modelNormal); + } else if (matrixMode == MATMODE_STATIC) { worldPos = staticModelMatrix * modelPos; worldNormal = mat3(staticModelMatrix) * modelNormal; - } else { + } else { // MATMODE_NORMAL // do interpolation Transform tx = Lerp( transforms[instData.x + 0u], diff --git a/rts/Rendering/Common/ModelDrawerState.cpp b/rts/Rendering/Common/ModelDrawerState.cpp index b732a8d22c7..3aaa4c8c94c 100644 --- a/rts/Rendering/Common/ModelDrawerState.cpp +++ b/rts/Rendering/Common/ModelDrawerState.cpp @@ -263,6 +263,10 @@ CModelDrawerStateGL4::CModelDrawerStateGL4() modelShaders[n]->SetFlag("GBUFFER_MISCTEX_IDX", GL::GeometryBuffer::ATTACHMENT_MISCTEX); modelShaders[n]->SetFlag("GBUFFER_ZVALTEX_IDX", GL::GeometryBuffer::ATTACHMENT_ZVALTEX); + // name the matrix-mode values the shader compares against, so it reads modes by name + modelShaders[n]->SetFlag("MATMODE_STATIC", static_cast(ShaderMatrixModes::STATIC_MATMODE)); + modelShaders[n]->SetFlag("MATMODE_ARRAY", static_cast(ShaderMatrixModes::ARRAY_MATMODE)); + modelShaders[n]->Link(); modelShaders[n]->Enable(); modelShaders[n]->Disable(); diff --git a/rts/Rendering/Models/3DModelVAO.cpp b/rts/Rendering/Models/3DModelVAO.cpp index 33025974957..ae9d82d799c 100644 --- a/rts/Rendering/Models/3DModelVAO.cpp +++ b/rts/Rendering/Models/3DModelVAO.cpp @@ -372,6 +372,22 @@ bool S3DModelVAO::AddToSubmission(const UnitDef* unitDef, uint16_t paletteIndex) return AddToSubmissionImpl(unitDef, model->indxStart, model->indxCount, paletteIndex); } +bool S3DModelVAO::AddStaticInstance(const S3DModel* model, uint32_t worldTransformOffset, uint16_t paletteIndex) +{ + RECOIL_DETAILED_TRACY_ZONE; + assert(model); + + // the world transform is the caller-supplied slot; pieces are read from the model bind pose + return EmplaceInstance( + model->indxStart, model->indxCount, + worldTransformOffset, + paletteIndex, + static_cast(model->numPieces), + static_cast(modelUniformsStorage.GetObjOffset(model)), + static_cast(transformsUploader.GetElemOffset(model)) + ); +} + void S3DModelVAO::Submit(GLenum mode, bool bindUnbind) { diff --git a/rts/Rendering/Models/3DModelVAO.hpp b/rts/Rendering/Models/3DModelVAO.hpp index d9405818f46..e64f3fba517 100644 --- a/rts/Rendering/Models/3DModelVAO.hpp +++ b/rts/Rendering/Models/3DModelVAO.hpp @@ -71,6 +71,9 @@ class S3DModelVAO { bool AddToSubmission(const CFeature* feature); bool AddToSubmission(const UnitDef* unitDef, uint16_t paletteIndex); + + bool AddStaticInstance(const S3DModel* model, uint32_t worldTransformOffset, uint16_t paletteIndex); + void Submit(GLenum mode = GL_TRIANGLES, bool bindUnbind = false); bool SubmitImmediately(const S3DModel* model, uint16_t paletteIndex, GLenum mode = GL_TRIANGLES, bool bindUnbind = false); diff --git a/rts/Rendering/Units/UnitDrawer.cpp b/rts/Rendering/Units/UnitDrawer.cpp index 2ff97165935..9a99cdf6d09 100644 --- a/rts/Rendering/Units/UnitDrawer.cpp +++ b/rts/Rendering/Units/UnitDrawer.cpp @@ -2,6 +2,8 @@ #include "UnitDrawer.h" +#include + #include "Game/Camera.h" #include "Game/CameraHandler.h" #include "Game/Game.h" @@ -1745,46 +1747,75 @@ void CUnitDrawerGL4::DrawAlphaObjects(int modelType, bool drawReflection, bool d void CUnitDrawerGL4::DrawGhostedBuildings(int modelType) const { RECOIL_DETAILED_TRACY_ZONE; - const auto& deadGhostBuildings = modelDrawerData->GetDeadGhostBuildings(gu->myAllyTeam, modelType); auto& smv = S3DModelVAO::GetInstance(); smv.Bind(); - const auto oldMM = modelDrawerState->SetMatrixMode(ShaderMatrixModes::STATIC_MATMODE); - // deadGhostedBuildings - { - modelDrawerState->SetColorMultiplier(0.6f, 0.6f, 0.6f, IModelDrawerState::alphaValues.y); - modelDrawerState->SetTeamColor(0, IModelDrawerState::alphaValues.y); //teamID doesn't matter here + // Ghost buildings are static (no animation, never move), so each gets a single world-transform + // slot in the transforms SSBO and is drawn batched through ARRAY_MATMODE - one multidraw per + // (color bucket x texture type) instead of one immediate draw per ghost. + const auto oldMM = modelDrawerState->SetMatrixMode(ShaderMatrixModes::ARRAY_MATMODE); - int prevModelType = -1; - int prevTexType = -1; - for (const auto* dgb : deadGhostBuildings) { - if (!camera->InView(dgb->pos, dgb->GetModel()->GetDrawRadius())) + struct GhostInstance { + const S3DModel* model; + uint32_t worldTransformOffset; + uint16_t paletteIndex; // color the ghost was last seen under (see LiveGhostBuilding / GhostSolidObject) + }; + // bind the texture once per group, accumulate, then one Submit (=one multidraw) per texture type. + // buckets are reused across frames (see clearBuckets) so a screen full of ghosts does not realloc + // its per-texture vectors every frame; empty buckets (a texture no longer on screen) are skipped. + const auto flushGhosts = [&](const std::map>& byTex) { + for (const auto& [texType, instances] : byTex) { + if (instances.empty()) continue; + CModelDrawerHelper::BindModelTypeTexture(modelType, texType); + for (const auto& gi : instances) + smv.AddStaticInstance(gi.model, gi.worldTransformOffset, gi.paletteIndex); + smv.Submit(GL_TRIANGLES, false); + } + }; + // clear the mapped vectors (keeping their capacity) instead of clearing the map (which would free them) + const auto clearBuckets = [](std::map>& byTex) { + for (auto& [texType, instances] : byTex) + instances.clear(); + }; - static CMatrix44f staticWorldMat; - - staticWorldMat.LoadIdentity(); - staticWorldMat.Translate(dgb->pos); + // deadGhostedBuildings (single color state) + { + const auto& deadGhostBuildings = modelDrawerData->GetDeadGhostBuildings(gu->myAllyTeam, modelType); - staticWorldMat.RotateY(-dgb->facing * math::DEG_TO_RAD * 90.0f); + static std::map> byTex; + clearBuckets(byTex); + bool any = false; + for (const auto* dgb : deadGhostBuildings) { + const S3DModel* model = dgb->GetModel(); + if (!camera->InView(dgb->pos, model->GetDrawRadius())) + continue; + if (!dgb->worldTransformAlloc.Valid()) + continue; - if (prevModelType != modelType || prevTexType != dgb->GetModel()->textureType) { - prevModelType = modelType; prevTexType = dgb->GetModel()->textureType; - CModelDrawerHelper::BindModelTypeTexture(modelType, dgb->GetModel()->textureType); //inefficient rendering, but w/e - } + byTex[model->textureType].push_back({ model, static_cast(dgb->worldTransformAlloc.GetOffset()), dgb->paletteIndex }); + any = true; + } - modelDrawerState->SetStaticModelMatrix(staticWorldMat); - smv.SubmitImmediately(dgb->GetModel(), dgb->paletteIndex); //need to submit immediately every model because of static per-model matrix + if (any) { + modelDrawerState->SetColorMultiplier(0.6f, 0.6f, 0.6f, IModelDrawerState::alphaValues.y); + modelDrawerState->SetTeamColor(0, IModelDrawerState::alphaValues.y); //teamID is per-instance + flushGhosts(byTex); } } - // liveGhostedBuildings + // liveGhostedBuildings (two color states: normal and CONTRADAR) { const auto& liveGhostedBuildings = modelDrawerData->GetLiveGhostBuildings(gu->myAllyTeam, modelType); - int prevModelType = -1; - int prevTexType = -1; + static std::map> byTexNormal; + static std::map> byTexContradar; + clearBuckets(byTexNormal); + clearBuckets(byTexContradar); + bool anyNormal = false; + bool anyContradar = false; + for (const auto& lgb : liveGhostedBuildings) { const CUnit* u = lgb.unit; if (!camera->InView(u->pos, u->model->GetDrawRadius())) @@ -1798,34 +1829,27 @@ void CUnitDrawerGL4::DrawGhostedBuildings(int modelType) const if (model->type != modelType) continue; - static CMatrix44f staticWorldMat; - - staticWorldMat.LoadIdentity(); - staticWorldMat.Translate(u->pos); - - staticWorldMat.RotateY(-u->buildFacing * math::DEG_TO_RAD * 90.0f); + const size_t xfOffset = modelDrawerData->GetLiveGhostTransform(u); + if (xfOffset == TransformsMemStorage::INVALID_INDEX) + continue; const unsigned short losStatus = u->losStatus[gu->myAllyTeam]; + const bool contradar = (losStatus & LOS_CONTRADAR); + // bucket with the palette the unit was last seen under, not the live unit's current one + (contradar ? byTexContradar : byTexNormal)[model->textureType] + .push_back({ model, static_cast(xfOffset), lgb.paletteIndex }); + (contradar ? anyContradar : anyNormal) = true; + } - // ghosted enemy units; SetTeamColor only gates the alpha here (the per-instance - // paletteIndex passed to SubmitImmediately drives the actual color) - if (losStatus & LOS_CONTRADAR) { - modelDrawerState->SetColorMultiplier(0.9f, 0.9f, 0.9f, IModelDrawerState::alphaValues.z); - modelDrawerState->SetTeamColor(lgb.team, IModelDrawerState::alphaValues.z); - } - else { - modelDrawerState->SetColorMultiplier(0.6f, 0.6f, 0.6f, IModelDrawerState::alphaValues.y); - modelDrawerState->SetTeamColor(lgb.team, IModelDrawerState::alphaValues.y); - } - - if (prevModelType != modelType || prevTexType != model->textureType) { - prevModelType = modelType; prevTexType = model->textureType; - CModelDrawerHelper::BindModelTypeTexture(modelType, model->textureType); //inefficient rendering, but w/e - } - - modelDrawerState->SetStaticModelMatrix(staticWorldMat); - // draw with the palette the unit was last seen under, not the live unit's current one - smv.SubmitImmediately(model, lgb.paletteIndex); //need to submit immediately every model because of static per-model matrix + if (anyNormal) { + modelDrawerState->SetColorMultiplier(0.6f, 0.6f, 0.6f, IModelDrawerState::alphaValues.y); + modelDrawerState->SetTeamColor(0, IModelDrawerState::alphaValues.y); + flushGhosts(byTexNormal); + } + if (anyContradar) { + modelDrawerState->SetColorMultiplier(0.9f, 0.9f, 0.9f, IModelDrawerState::alphaValues.z); + modelDrawerState->SetTeamColor(0, IModelDrawerState::alphaValues.z); + flushGhosts(byTexContradar); } } diff --git a/rts/Rendering/Units/UnitDrawerData.cpp b/rts/Rendering/Units/UnitDrawerData.cpp index 8059312f907..dfd048a332a 100644 --- a/rts/Rendering/Units/UnitDrawerData.cpp +++ b/rts/Rendering/Units/UnitDrawerData.cpp @@ -29,9 +29,22 @@ #include "Map/ReadMap.h" #include "System/Misc/TracyDefs.h" +#include "System/Matrix44f.h" +#include "System/MathConstants.h" +#include "System/Transform.hpp" +#include "Rendering/Models/ModelsMemStorage.h" static FixedDynMemPoolT ghostMemPool; +// world transform of a (static) ghost building, matching the legacy staticModelMatrix construction +static Transform MakeGhostWorldTransform(const float3& pos, int facing) +{ + CMatrix44f m; + m.Translate(pos); + m.RotateY(-facing * math::HALFPI); + return Transform::FromMatrix(m); +} + /////////////////////////// CR_BIND_POOL(GhostSolidObject, ,ghostMemPool.allocMem, ghostMemPool.freeMem) @@ -51,6 +64,7 @@ CR_REG_METADATA(GhostSolidObject, ( CR_MEMBER(paletteIndex), CR_IGNORED(currentIconIndex), + CR_IGNORED(worldTransformAlloc), CR_IGNORED(model), CR_POSTLOAD(PostLoad) @@ -96,6 +110,15 @@ void GhostSolidObject::PostLoad() RECOIL_DETAILED_TRACY_ZONE; model = nullptr; GetModel(); + + // the GPU world transform slot is render-only state; re-create it from the saved pos/facing + InitWorldTransform(); +} + +void GhostSolidObject::InitWorldTransform() +{ + worldTransformAlloc = ScopedTransformMemAlloc(1); + worldTransformAlloc.UpdateForced(0, MakeGhostWorldTransform(pos, facing)); } const S3DModel* GhostSolidObject::GetModel() const @@ -165,6 +188,7 @@ CUnitDrawerData::~CUnitDrawerData() if (tmpGso->DecRef()) continue; + // worldTransformAlloc frees its slot in ~GhostSolidObject (ghostMemPool.free below) // might be the gbOwner of a decal; groundDecals is deleted after us groundDecals->GhostDestroyed(tmpGso); ghostMemPool.free(tmpGso); @@ -173,6 +197,7 @@ CUnitDrawerData::~CUnitDrawerData() lgb.clear(); } } + liveGhostTransforms.clear(); // each entry's ScopedTransformMemAlloc frees its slot on erase assert(ghostMemPool.allocs() == 0); ghostMemPool.clear(); @@ -224,6 +249,8 @@ void CUnitDrawerData::Update() updateBody(unit); } + UpdateLiveGhostTransforms(); + if ((useDistToGroundForIcons = (camHandler->GetCurrentController()).GetUseDistToGroundForIcons())) { const float3& camPos = camera->GetPos(); // use the height at the current camera position @@ -634,6 +661,8 @@ bool CUnitDrawerData::UpdateUnitGhosts(const CUnit* unit, const bool addNewGhost gso->iconRadius = u->iconRadius; + gso->InitWorldTransform(); + groundDecals->GhostCreated(u, gso); } @@ -714,6 +743,37 @@ void CUnitDrawerData::UnitLeftLos(const CUnit* unit, int allyTeam) UpdateCurrentUnitIcon(unit); } +void CUnitDrawerData::UpdateLiveGhostTransforms() +{ + RECOIL_DETAILED_TRACY_ZONE; + // Maintain one world-transform slot per live ghost building drawn for the local allyTeam. + // Ghosts are static, so each slot is filled once on first sight; entries not seen this sweep + // (units that regained LOS, died, or belong to a different allyTeam now) are freed. + const int stamp = ++liveGhostSweepStamp; + + for (int modelType = MODELTYPE_3DO; modelType < MODELTYPE_CNT; modelType++) { + for (const auto& lgb : savedData.liveGhostBuildings[gu->myAllyTeam][modelType]) { + const CUnit* u = lgb.unit; + const auto it = liveGhostTransforms.find(u); + if (it == liveGhostTransforms.end()) { + ScopedTransformMemAlloc alloc(1); + alloc.UpdateForced(0, MakeGhostWorldTransform(u->pos, u->buildFacing)); + liveGhostTransforms.emplace(u, std::make_pair(std::move(alloc), stamp)); + } + else { + it->second.second = stamp; + } + } + } + + for (auto it = liveGhostTransforms.begin(); it != liveGhostTransforms.end(); ) { + if (it->second.second != stamp) + it = liveGhostTransforms.erase(it); // ScopedTransformMemAlloc frees the slot on erase + else + ++it; + } +} + void CUnitDrawerData::UnitLeavesGhostChanged(const CUnit* unit, const bool leaveDeadGhost) { if (unit->leavesGhost) { @@ -750,6 +810,7 @@ void CUnitDrawerData::PlayerChanged(int playerID) void CUnitDrawerData::RemoveDeadGhost(GhostSolidObject* gso, std::vector& dgb, int index) { if (!gso->DecRef()) { + // worldTransformAlloc frees its slot in ~GhostSolidObject (ghostMemPool.free) groundDecals->GhostDestroyed(gso); ghostMemPool.free(gso); } diff --git a/rts/Rendering/Units/UnitDrawerData.h b/rts/Rendering/Units/UnitDrawerData.h index a2debe2b7bb..d5ae046da63 100644 --- a/rts/Rendering/Units/UnitDrawerData.h +++ b/rts/Rendering/Units/UnitDrawerData.h @@ -23,6 +23,8 @@ class GhostSolidObject { bool DecRef() { return ((refCount--) > 1); } const S3DModel* GetModel() const; void PostLoad(); + // (re)allocate and fill the batched-draw world transform slot from pos/facing + void InitWorldTransform(); public: std::string modelName; @@ -42,6 +44,8 @@ class GhostSolidObject { uint16_t paletteIndex; size_t currentIconIndex; + + ScopedTransformMemAlloc worldTransformAlloc; private: mutable const S3DModel* model; }; @@ -167,6 +171,12 @@ class CUnitDrawerData : public CUnitDrawerDataBase { return savedData.liveGhostBuildings[allyTeam][modelType]; } + // world transform slot (in transformsMemStorage) for a live ghost building; INVALID_INDEX if none + size_t GetLiveGhostTransform(const CUnit* unit) const { + const auto it = liveGhostTransforms.find(unit); + return (it != liveGhostTransforms.end()) ? it->second.first.GetOffset(false) : TransformsMemStorage::INVALID_INDEX; + } + auto* GetSavedData() { return &savedData; } const auto* GetSavedData() const { return &savedData; } protected: @@ -207,6 +217,13 @@ class CUnitDrawerData : public CUnitDrawerDataBase { S3DModel* GetUnitModel(const CUnit* unit) const; void RemoveDeadGhost(GhostSolidObject* gso, std::vector& dgb, int index); + // rebuilds the per-unit world transform slots for live ghost buildings of the local allyTeam. + // scan-based so it self-heals across savegame load, allyTeam/spectator changes and leavesGhost toggles. + void UpdateLiveGhostTransforms(); + // maps unit -> { RAII-owned world transform slot, last sweep stamp seen } + spring::unordered_map> liveGhostTransforms; + int liveGhostSweepStamp = 0; + // icons bool useDistToGroundForIcons; float sqCamDistToGroundForIcons; From e238786d07a1f4e7b5c17d7154aa5ce84966d396 Mon Sep 17 00:00:00 2001 From: Nouridin <39697812+Nouridin@users.noreply.github.com> Date: Wed, 29 Jul 2026 12:48:43 +0300 Subject: [PATCH 79/90] Changes: (#3066) - Removed `lcstr` and `lcsub` arrays to save 64KB of BSS data per translation. - Removed unsafe inline implementation of `StrCaseStr` to eliminate multi-threaded race conditions. --- rts/Game/UI/MiniMap.cpp | 2 +- rts/System/StringUtil.h | 20 -------------------- 2 files changed, 1 insertion(+), 21 deletions(-) diff --git a/rts/Game/UI/MiniMap.cpp b/rts/Game/UI/MiniMap.cpp index 2f202bffc18..30037a609c1 100644 --- a/rts/Game/UI/MiniMap.cpp +++ b/rts/Game/UI/MiniMap.cpp @@ -457,7 +457,7 @@ void CMiniMap::ConfigCommand(const std::string& line) const bool wantMaximized = (words.size() >= 2) ? !!atoi(words[1].c_str()) : !isMaximized; if (isMaximized != wantMaximized) - ToggleMaximized(StrCaseStr(words[0].c_str(), "maxspect") == 0); + ToggleMaximized(hashStringLower(words[0].c_str()) != hashString("maxspect")); } break; case hashString("mouseevents"): { diff --git a/rts/System/StringUtil.h b/rts/System/StringUtil.h index ea7ca4b62c6..73d2aaeef26 100644 --- a/rts/System/StringUtil.h +++ b/rts/System/StringUtil.h @@ -30,26 +30,6 @@ struct _UTIL_CONCAT(doOnce, __LINE__) { _UTIL_CONCAT(doOnce, __LINE__)() { code; } }; static _UTIL_CONCAT(doOnce, __LINE__) _UTIL_CONCAT(doOnceVar, __LINE__); -static char lcstr[32768]; -static char lcsub[32768]; -static inline const char* StrCaseStr(const char* str, const char* sub) { - const char* pos = nullptr; - - if (str == nullptr) - return nullptr; - if (sub == nullptr) - return nullptr; - - std::strncpy(lcstr, str, sizeof(lcstr) - 1); - std::strncpy(lcsub, sub, sizeof(lcsub) - 1); - std::transform(lcstr, lcstr + sizeof(lcstr), lcstr, (int (*)(int)) tolower); - std::transform(lcsub, lcsub + sizeof(lcsub), lcsub, (int (*)(int)) tolower); - - if ((pos = std::strstr(lcstr, lcsub)) == nullptr) - return nullptr; - - return (str + (pos - lcstr)); -} static inline void StringToLower(const char* in, char* out, size_t len) { From bdd7662b089d5e4f3cd18b0271bc247a27acd36b Mon Sep 17 00:00:00 2001 From: color-dev <239244564+color-dev@users.noreply.github.com> Date: Wed, 29 Jul 2026 02:51:58 -0700 Subject: [PATCH 80/90] MouseHandler: route XButtons (Mouse4/5) as keybinds instead of mouse ownership (#2613) When bound through uikeys, Mouse4/5 (aka XButtons) try to assign ownership to gameInputReceiver. This causes either the mouse input or the X-button bind to be misrouted when pressed in combination. To prevent this, Mouse4/5 are routed like keybinds instead of sharing mouse ownership. This allows them to be pressed/released without stealing or confusing the activeReceiver. --- rts/Game/UI/MouseHandler.cpp | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/rts/Game/UI/MouseHandler.cpp b/rts/Game/UI/MouseHandler.cpp index 400cffa743f..62e155622b7 100644 --- a/rts/Game/UI/MouseHandler.cpp +++ b/rts/Game/UI/MouseHandler.cpp @@ -361,6 +361,24 @@ void CMouseHandler::MousePress(int x, int y, int button) pressedBitMask |= 1 << button; + const bool isXButton = (button == SDL_BUTTON_X1 || button == SDL_BUTTON_X2); + if (isXButton) { + + // 1. Lua first + if (luaInputReceiver->MousePress(x, y, button)) { + return; + } + + // 2. GameInputReceiver via the same path as mouse buttons + auto activeControllerReceiver = (activeController == nullptr) ? nullptr : activeController->GetInputReceiver(); + if (activeControllerReceiver && activeControllerReceiver->MousePress(x, y, button)) { + // NOTE: X‑buttons bypass ownership so they can be pressed/released without stealing or confusing activeReceiver + return; + } + return; + } + + if (activeReceiver != nullptr && activeReceiver->MousePress(x, y, button)) return; @@ -530,6 +548,22 @@ void CMouseHandler::MouseRelease(int x, int y, int button) return; } + const bool isXButton = (button == SDL_BUTTON_X1 || button == SDL_BUTTON_X2); + if (isXButton) { + + // 1. Lua first + luaInputReceiver->MouseRelease(x, y, button); + + // 2. GameInputReceiver via the same path as mouse buttons + auto activeControllerReceiver = (activeController == nullptr) ? nullptr : activeController->GetInputReceiver(); + if (activeControllerReceiver) { + activeControllerReceiver->MouseRelease(x, y, button); + } + + // 3. Skip ownership funnel + return; + } + if (activeReceiver != nullptr) { activeReceiver->MouseRelease(x, y, button); From 3d1e96185c2c289d35e78ff1aeba78aea5bf9adc Mon Sep 17 00:00:00 2001 From: lostsquirrel1 Date: Wed, 29 Jul 2026 11:05:27 +0100 Subject: [PATCH 81/90] Update changelog --- doc/site/content/changelogs/_index.markdown | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/doc/site/content/changelogs/_index.markdown b/doc/site/content/changelogs/_index.markdown index 1e8e738da3a..0906e9055e3 100644 --- a/doc/site/content/changelogs/_index.markdown +++ b/doc/site/content/changelogs/_index.markdown @@ -57,6 +57,7 @@ This is the bleeding-edge changelog since version 2026.06, for **pre-release 202 - Added cancelcommand action - Added GetPrevFrameChecksum() to the Lua API [PR 2922](https://github.com/beyond-all-reason/RecoilEngine/pull/2922) - Fixes to Spring.SetMapShader [PR 3127](https://github.com/beyond-all-reason/RecoilEngine/pull/3127) +- MouseHandler: route XButtons (Mouse4/5) as keybinds instead of mouse ownership [PR 2613](https://github.com/beyond-all-reason/RecoilEngine/pull/2613) ## Misc - Restored lowercasing in FileSystem::GetExtension @@ -80,6 +81,7 @@ This is the bleeding-edge changelog since version 2026.06, for **pre-release 202 - Fix stale spGetActionHotKeys returns [PR 3082](https://github.com/beyond-all-reason/RecoilEngine/pull/3082) - Use a portable type cast in MemPoolTypes logging - Support alternate file extensions for replays [PR 2975](https://github.com/beyond-all-reason/RecoilEngine/pull/2975) +- Throw error and stop processing if modrules parsing fails ## Rendering - Added sorting icon names before adding to atlas so insertion order is consistent across runs. @@ -96,6 +98,8 @@ This is the bleeding-edge changelog since version 2026.06, for **pre-release 202 - Debug logs for loading splat normals - Fix SMF DNTS gating and fallback textures - Add mapoptions for blank map splats +- Performance improvements with drawing ghosted buildings [PR 3110](https://github.com/beyond-all-reason/RecoilEngine/pull/3110) +- Changed ghosted buildings are drawn based on the last team was seen with and so which team ownership doesn't show automatically on the ghost. [PR 3108](https://github.com/beyond-all-reason/RecoilEngine/pull/3108) ## Simulation - Sanitize NaNs in CHoverAirMoveType::UpdateMoveRate() @@ -106,3 +110,4 @@ This is the bleeding-edge changelog since version 2026.06, for **pre-release 202 - Fixed edge scrolling threshold [Issue 2987](https://github.com/beyond-all-reason/RecoilEngine/issues/2987) - Dump state handles resource packs - Fix units having the wrong path id after loading a save game. [PR 3120](https://github.com/beyond-all-reason/RecoilEngine/pull/3120) +- Avoid UB in float-to-short angle casts (fixes arm64/x86 desync) [PR 3075](https://github.com/beyond-all-reason/RecoilEngine/pull/3075) From 3fdeec025da39f06659076785bbfbc388b66c846 Mon Sep 17 00:00:00 2001 From: TarnishedKnight Date: Wed, 29 Jul 2026 12:31:11 +0100 Subject: [PATCH 82/90] Bar 106 fix publish workflow (#3146) --- doc/site/mise.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/doc/site/mise.toml b/doc/site/mise.toml index 3eac83faf22..e827931ebe8 100644 --- a/doc/site/mise.toml +++ b/doc/site/mise.toml @@ -1,3 +1,6 @@ +[settings.npm] + package_manager = "npm" + [tools] # We only install tools required for each task on each task, unless all tasks use a tool here # For CI when we need to install and cache all tools, see mise.ci.toml @@ -101,7 +104,7 @@ [tasks.lua_library] description = "Generate Lua Docs" - tools."npm:lua-doc-extractor" = "{{env.LUA_DOC_EXTRACTOR_VERSION}}" + tools."npm:lua-doc-extractor" = { version = "{{env.LUA_DOC_EXTRACTOR_VERSION}}", allow_low_downloads = true } dir = "../../" run = [ "lua-doc-extractor --src \"{{vars.lua_doc_paths}}\" --dest {{vars.lua_doc_gen_dest}} --repo \"${LUA_DOC_EXTRACTOR_SOURCE_REF}\"" From e39c69ae7af257d20f3fa5df7210632520fa4424 Mon Sep 17 00:00:00 2001 From: lostsquirrel1 Date: Wed, 29 Jul 2026 12:43:00 +0100 Subject: [PATCH 83/90] fix changelog page 2026.06 --- doc/site/content/changelogs/changelog-2026-06.markdown | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/doc/site/content/changelogs/changelog-2026-06.markdown b/doc/site/content/changelogs/changelog-2026-06.markdown index 993a030f907..90a9f6ee9ec 100644 --- a/doc/site/content/changelogs/changelog-2026-06.markdown +++ b/doc/site/content/changelogs/changelog-2026-06.markdown @@ -1,8 +1,6 @@ +++ -title = "Running changelog" -[cascade] - [cascade.params] - type = "docs" +title = "Release 2026.06" +aliases = ['/changelogs/changelog-2026-06'] +++ This is the bleeding-edge changelog since version 2025.06, for **pre-release 2026.06**. From d6e9943238dc50dc7e4ebab8694ea66ac4c55835 Mon Sep 17 00:00:00 2001 From: Tom J Nowell Date: Wed, 29 Jul 2026 12:54:08 +0100 Subject: [PATCH 84/90] smmalloc: drop the leaked INLINE macro in a single wrapper header (#3026) smmalloc.h defines `#define INLINE inline`, a very generic token that collides with unrelated code (e.g. simdjson's layout_mode::INLINE enumerator) whenever both land in the same translation unit. Instead of undef-ing INLINE at each affected site, add System/recoil-smmalloc.h which includes smmalloc and drops the macro, and route the engine's single smmalloc include (MemPoolTypes.h) through it. The macro can no longer escape into engine code, so a future file mixing smmalloc with json/etc. won't reintroduce the collision. Surfaced by the macOS port (#2991, commit 16622a9707). The upstream smmalloc leak is worth fixing there too. Co-authored-by: Mark Kropf --- rts/System/MemPoolTypes.h | 2 +- rts/System/recoil-smmalloc.h | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 rts/System/recoil-smmalloc.h diff --git a/rts/System/MemPoolTypes.h b/rts/System/MemPoolTypes.h index 3ab106b64b9..34e0824d50a 100644 --- a/rts/System/MemPoolTypes.h +++ b/rts/System/MemPoolTypes.h @@ -13,7 +13,7 @@ #include #include -#include "smmalloc/smmalloc.h" +#include "System/recoil-smmalloc.h" #include "System/UnorderedMap.hpp" #include "System/ContainerUtil.h" diff --git a/rts/System/recoil-smmalloc.h b/rts/System/recoil-smmalloc.h new file mode 100644 index 00000000000..4ee7b9cb246 --- /dev/null +++ b/rts/System/recoil-smmalloc.h @@ -0,0 +1,15 @@ +/* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ + +#pragma once + +// smmalloc.h leaks `#define INLINE inline` -- a very generic token that +// collides with unrelated code (e.g. simdjson's layout_mode::INLINE +// enumerator) whenever both end up in the same translation unit. Include +// smmalloc only through this wrapper so the macro is dropped immediately and +// never escapes into engine code. + +#include "smmalloc/smmalloc.h" + +#ifdef INLINE +#undef INLINE +#endif From ae77ffd3c55af20d045fcdbbeba179bd39e3c9e3 Mon Sep 17 00:00:00 2001 From: Alexander Heistermann Date: Wed, 29 Jul 2026 08:43:40 -0500 Subject: [PATCH 85/90] Remove all instances of 32 bit support from cmake file (#3033) Co-authored-by: 12345swordy <12345swordy@user.noreply.github.com> --- CMakeLists.txt | 37 +++++++++---------------------------- 1 file changed, 9 insertions(+), 28 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 243ef8cb697..7b3cfb886f2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -100,13 +100,12 @@ endif (APPLE) ### Compiler flags and defines based on build type include(TestCXXFlags) -## 32bit or 64bit? +## 64-bit architecture check set(MARCH_FLAG ${MARCH} CACHE STRING "CPU optimization (use `generic` for generic optimization)") -if (CMAKE_SIZEOF_VOID_P EQUAL 8) - set(MARCH_BITS 64 CACHE INTERNAL "" FORCE) -else (CMAKE_SIZEOF_VOID_P EQUAL 8) - message(FATAL_ERROR "RecoilEngine does not support 32 bits." ) -endif (CMAKE_SIZEOF_VOID_P EQUAL 8) +if (NOT CMAKE_SIZEOF_VOID_P EQUAL 8) + message(FATAL_ERROR "RecoilEngine requires a 64-bit build environment.") +endif (NOT CMAKE_SIZEOF_VOID_P EQUAL 8) +set(MARCH_BITS 64 CACHE INTERNAL "" FORCE) # Detect architecture if (CMAKE_SYSTEM_PROCESSOR MATCHES "arm64|aarch64|armv8|ARM64|AARCH64") @@ -119,9 +118,8 @@ else() message(FATAL_ERROR "Unsupported architecture: ${CMAKE_SYSTEM_PROCESSOR}. Only ARM64 and x86-64 are supported.") endif() -message(STATUS "Building Spring on ${MARCH_BITS}bit environment") -set(BUILD_BITS "${MARCH_BITS}" CACHE STRING "Target arch machine type") -message(STATUS "Targetting ${BUILD_BITS}bit") +message(STATUS "Building Spring on a 64-bit environment") +set(BUILD_BITS 64 CACHE INTERNAL "Target architecture bitness" FORCE) ### Install paths (relative to CMAKE_INSTALL_PREFIX) @@ -489,12 +487,7 @@ if (CMAKE_COMPILER_IS_GNUCXX) set(FALLBACK_SSE_FLAGS "${FALLBACK_SSE_FLAGS} -mno-avx -mno-fma -mno-fma4 -mno-xop -mno-lwp") set(FALLBACK_SSE_FLAGS "${FALLBACK_SSE_FLAGS} -mno-avx2") - if (MARCH_BITS EQUAL 64) - set(FALLBACK_MARCH "x86-64") - else (MARCH_BITS EQUAL 64) - set(FALLBACK_MARCH "i686") - endif (MARCH_BITS EQUAL 64) - + set(FALLBACK_MARCH "x86-64") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -march=${FALLBACK_MARCH} -mtune=generic ${FALLBACK_SSE_FLAGS}") endif (NOT MARCH_FLAG STREQUAL "generic") @@ -581,22 +574,10 @@ else (MSVC) #set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -ggdb3") #cmake has -g it by default #set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO} -ggdb3") #cmake has -g it by default - if (MARCH_BITS EQUAL 64 AND BUILD_BITS EQUAL 32) - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -m32") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -m32") - endif (MARCH_BITS EQUAL 64 AND BUILD_BITS EQUAL 32) if (MINGW) set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static -static-libgcc -static-libstdc++ -Wl,--enable-auto-import") set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -static -static-libgcc -static-libstdc++ -Wl,--enable-auto-import") - if (BUILD_BITS EQUAL 32) - # Increase memory limit from 2GB to 3GB on 32bit Windows and 2GB->4GB on 64bit Windows (assuming spring.exe is 32bit) - # http://msdn.microsoft.com/en-us/library/windows/desktop/aa366778(v=vs.85).aspx - message(STATUS "Enable IMAGE_FILE_LARGE_ADDRESS_AWARE (>2GB memory limit)") - set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--large-address-aware") - else (BUILD_BITS EQUAL 32) - message(WARNING "The 64bit version of spring on windows is experimental and may not sync with regular builds") - endif (BUILD_BITS EQUAL 32) endif (MINGW) endif (MSVC) @@ -723,4 +704,4 @@ if (INSTALL_PORTABLE) install(DIRECTORY DESTINATION maps) file(TOUCH ${CMAKE_CURRENT_BINARY_DIR}/springsettings_portable.cfg) # empty settings file for portable install install(FILES ${CMAKE_CURRENT_BINARY_DIR}/springsettings_portable.cfg DESTINATION . RENAME springsettings.cfg) -endif (INSTALL_PORTABLE) +endif (INSTALL_PORTABLE) \ No newline at end of file From 694a8320dd07271a64c6d22cbc48cd20c6cbad61 Mon Sep 17 00:00:00 2001 From: loveridge Date: Wed, 29 Jul 2026 13:47:42 +0000 Subject: [PATCH 86/90] send gameprogress packet on connection initialization (#2872) --- rts/Net/GameServer.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/rts/Net/GameServer.cpp b/rts/Net/GameServer.cpp index 4f8a7800c50..5be3e138612 100644 --- a/rts/Net/GameServer.cpp +++ b/rts/Net/GameServer.cpp @@ -3094,6 +3094,12 @@ unsigned CGameServer::BindConnection( } } + // inform player of the current frame + if (gameHasStarted) { + CBaseNetProtocol::PacketType progressPacket = CBaseNetProtocol::Get().SendCurrentFrameProgress(serverFrameNum); + newPlayer.SendData(progressPacket); + } + // finally send player all packets he missed until now for (const std::shared_ptr& p: packetCache) newPlayer.SendData(p); From 586b1cd9a0235325f722fa3290d58fb5c625025f Mon Sep 17 00:00:00 2001 From: Fx-Doo Date: Wed, 29 Jul 2026 16:50:46 +0200 Subject: [PATCH 87/90] Explicitly use InSonarJammer or InJammer depending on the tested sensor (#2980) * Explicitly use InSonarJammer or InJammer depending on the tested sensor * Update IsUnitInJammer and GetPositionLosState to return either InJammer() or InSonarJammer() depending on pos/unitpos return args: IsUnitInJammer() and select(4, GetPositionLosState()) true -> complete jam = can't be sensed by anything false -> no jam or incomplete jam = can be sensed I picked this update because it's the most consistent with what we had. The main difference being that we check before calling InJammer() or InSonarJammer(), rather than having that pos check and jammerType dispatch within InJammer() * Revert "Update IsUnitInJammer and GetPositionLosState to return either InJammer() or InSonarJammer() depending on pos/unitpos" This reverts commit 93b8fb528317c2a07bedaf5fc5aa505f1fafc6cf. --- rts/Sim/Misc/LosHandler.cpp | 40 +++++++++++++++++++++++++------------ rts/Sim/Misc/LosHandler.h | 9 +++++++-- 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/rts/Sim/Misc/LosHandler.cpp b/rts/Sim/Misc/LosHandler.cpp index a132ddeb382..5b92c0d1762 100644 --- a/rts/Sim/Misc/LosHandler.cpp +++ b/rts/Sim/Misc/LosHandler.cpp @@ -951,22 +951,24 @@ bool CLosHandler::InRadar(const float3 pos, int allyTeam) const bool CLosHandler::InRadar(const CUnit* unit, int allyTeam) const { RECOIL_DETAILED_TRACY_ZONE; - // unit is discoverable by sonar + // first attempt to discover with sonar: + // unit is discoverable by sonar only if not sonarStealth or not sonarJammed if (unit->IsInWater()) { if ((!unit->sonarStealth || unit->beingBuilt) && sonar.InSight(unit->pos, allyTeam) && - !InJammer(unit, allyTeam)) + !InSonarJammer(unit, allyTeam)) return true; } - // unit is completely submerged, only sonar can see it + // unit is InWater and UnderWater, but was not previously caught by sonar, skip it if (unit->IsUnderWater()) return false; - // radar stealth + // then attempt to discover with radar + // unit is radar stealth, can't be discovered if (unit->stealth && !unit->beingBuilt) return false; - + // use radar jamming, not sonar jamming here return (radar.InSight(unit->pos, allyTeam) && !InJammer(unit, allyTeam)); } @@ -976,13 +978,9 @@ bool CLosHandler::InJammer(const float3 pos, int allyTeam) const RECOIL_DETAILED_TRACY_ZONE; const int jammerAlly = modInfo.separateJammers ? allyTeam : 0; - if (pos.y < 0.0f) - return sonarJammer.InSight(pos, jammerAlly); - return jammer.InSight(pos, jammerAlly); } - bool CLosHandler::InJammer(const CUnit* unit, int allyTeam) const { RECOIL_DETAILED_TRACY_ZONE; @@ -990,11 +988,27 @@ bool CLosHandler::InJammer(const CUnit* unit, int allyTeam) const return false; //TODO handle ingame alliances + const int jammerAlly = modInfo.separateJammers ? unit->allyteam : 0; + + return jammer.InSight(unit->pos, jammerAlly); +} + +bool CLosHandler::InSonarJammer(const float3 pos, int allyTeam) const +{ + RECOIL_DETAILED_TRACY_ZONE; + const int jammerAlly = modInfo.separateJammers ? allyTeam : 0; + + return sonarJammer.InSight(pos, jammerAlly); +} +bool CLosHandler::InSonarJammer(const CUnit* unit, int allyTeam) const +{ + RECOIL_DETAILED_TRACY_ZONE; + if (allyTeam == unit->allyteam) + return false; + + //TODO handle ingame alliances const int jammerAlly = modInfo.separateJammers ? unit->allyteam : 0; - if (unit->IsUnderWater()) { - return sonarJammer.InSight(unit->pos, jammerAlly); - } - return jammer.InSight(unit->pos, jammerAlly); + return sonarJammer.InSight(unit->pos, jammerAlly); } diff --git a/rts/Sim/Misc/LosHandler.h b/rts/Sim/Misc/LosHandler.h index e30c4668bc1..9f17fa3ebc3 100644 --- a/rts/Sim/Misc/LosHandler.h +++ b/rts/Sim/Misc/LosHandler.h @@ -253,11 +253,16 @@ class CLosHandler : public CEventClient bool InRadar(const CUnit* unit, int allyTeam) const; - // returns whether a square is being radar- or sonar-jammed - // (even when the square is not in radar- or sonar-coverage) + // returns whether a square is being radar-jammed + // (even when the square is not in radar-coverage) bool InJammer(const float3 pos, int allyTeam) const; bool InJammer(const CUnit* unit, int allyTeam) const; + // returns whether a square is being sonar-jammed + // (even when the square is not in sonar-coverage) + bool InSonarJammer(const float3 pos, int allyTeam) const; + bool InSonarJammer(const CUnit* unit, int allyTeam) const; + bool InSeismicDistance(const CUnit* unit, int allyTeam) const { return seismic.InSight(unit->pos, allyTeam); From e7f84880a87e3c9c374cbd8e4b50482be8b78ee7 Mon Sep 17 00:00:00 2001 From: Keith Harvey Date: Wed, 29 Jul 2026 09:11:05 -0600 Subject: [PATCH 88/90] fix: minor Lua EmmyLua type annotations (#2888) * Lua: minor type annotation and doc fixes Series of minor EmmyLua annotation corrections and small documentation fixes that accumulated during the spring-split exploration. None of these depend on or relate to the spring-split refactor; pulled out as a standalone PR to keep the spring-split review focused. * expand check-lua to include Units/Scripts to capture UnitScriptTable in the shallow checkout --- .github/workflows/check-lua.yml | 3 + doc/site/mise.toml | 2 +- rts/Lua/LuaFonts.cpp | 77 ++++++++ rts/Lua/LuaObjectRendering.cpp | 131 +++++++++++++ rts/Lua/LuaOpenGL.cpp | 28 +-- rts/Lua/LuaPathFinder.cpp | 48 +++++ rts/Lua/LuaRules.cpp | 3 + rts/Lua/LuaSyncedCtrl.cpp | 119 ++++++------ rts/Lua/LuaSyncedRead.cpp | 126 +++++++++++-- rts/Lua/LuaUI.cpp | 6 + rts/Lua/LuaUnsyncedCtrl.cpp | 136 +++++++------- rts/Lua/LuaUnsyncedRead.cpp | 7 +- rts/Lua/LuaVBOImpl.cpp | 2 +- rts/Sim/Units/Scripts/LuaUnitScript.cpp | 233 ++++++++++++++++++++++++ 14 files changed, 769 insertions(+), 152 deletions(-) diff --git a/.github/workflows/check-lua.yml b/.github/workflows/check-lua.yml index 3e6e87a05c6..dcbef82f105 100644 --- a/.github/workflows/check-lua.yml +++ b/.github/workflows/check-lua.yml @@ -5,6 +5,7 @@ on: paths: - rts/Lua/** - rts/Rml/SolLua/**/*.cpp + - rts/Sim/Units/Scripts/**/*.cpp - doc/site/mise.toml push: branches: @@ -12,6 +13,7 @@ on: paths: - rts/Lua/** - rts/Rml/SolLua/**/*.cpp + - rts/Sim/Units/Scripts/**/*.cpp - doc/site/mise.toml env: RECOIL_LUA_LIBRARY_DIR: "rts/Lua/library" @@ -29,6 +31,7 @@ jobs: sparse-checkout: | rts/Lua rts/Rml/SolLua + rts/Sim/Units/Scripts doc/site/mise.toml - uses: jdx/mise-action@v2 diff --git a/doc/site/mise.toml b/doc/site/mise.toml index e827931ebe8..c473389ac1d 100644 --- a/doc/site/mise.toml +++ b/doc/site/mise.toml @@ -20,7 +20,7 @@ lua_doc_gen_dest = "$RECOIL_LUA_LIBRARY_DIR/generated" site_dir = 'doc/site' site_temp_dir = "{{vars.site_dir}}/temp" - lua_doc_paths = 'rts/{Lua,Rml/SolLua}/**/*.cpp' + lua_doc_paths = 'rts/{Lua,Rml/SolLua,Sim/Units/Scripts}/**/*.cpp' lua_pages_dir = "{{vars.site_dir}}/content/docs/lua-api" emmylua_cli_template = "{{vars.site_dir}}/emmylua-doc-cli-template" latest_release_data = 'data/latest_release.json' diff --git a/rts/Lua/LuaFonts.cpp b/rts/Lua/LuaFonts.cpp index 8bc2acda23f..c66908131c5 100644 --- a/rts/Lua/LuaFonts.cpp +++ b/rts/Lua/LuaFonts.cpp @@ -192,6 +192,15 @@ int LuaFonts::meta_index(lua_State* L) /******************************************************************************/ /******************************************************************************/ +/*** Load a font from a file. + * + * @function gl.LoadFont + * @param fontFile string + * @param size integer? + * @param outlineWidth integer? + * @param outlineWeight number? + * @return LuaFont font + */ int LuaFonts::LoadFont(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; @@ -208,6 +217,11 @@ int LuaFonts::LoadFont(lua_State* L) } +/*** Delete a font object. + * + * @function gl.DeleteFont + * @param font LuaFont + */ int LuaFonts::DeleteFont(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; @@ -261,6 +275,15 @@ int LuaFonts::ClearFallbackFonts(lua_State* L) /******************************************************************************/ /******************************************************************************/ +/*** Draws text in screen space at the given position. + * + * @function LuaFont:Print + * @param text string + * @param x number + * @param y number + * @param size number? Defaults to the font's point size. + * @param options string? Flag characters for alignment, outline, shadow, scaling, etc. (e.g. `"co"` for center and outline). + */ int LuaFonts::Print(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; @@ -309,6 +332,16 @@ int LuaFonts::Print(lua_State* L) return 0; } +/*** Draws text in world space at the given position. + * + * @function LuaFont:PrintWorld + * @param text string + * @param x number + * @param y number + * @param z number + * @param size number? Defaults to the font's point size. + * @param options string? Flag characters for alignment, outline, shadow, scaling, etc. (e.g. `"co"` for center and outline). + */ int LuaFonts::PrintWorld(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; @@ -388,6 +421,10 @@ int LuaFonts::Begin(lua_State* L) return 0; } +/*** Ends a font command block started with `LuaFont:Begin`. + * + * @function LuaFont:End + */ int LuaFonts::End(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; @@ -426,6 +463,16 @@ int LuaFonts::SubmitBuffered(lua_State* L) /******************************************************************************/ /******************************************************************************/ +/*** Wraps text to fit within a maximum width (and optional max height), in-place. + * + * @function LuaFont:WrapText + * @param text string + * @param maxWidth number + * @param maxHeight number? Defaults to an engine-defined maximum height. + * @param size number? Defaults to the font's point size. + * @return string wrappedText + * @return number lineCount + */ int LuaFonts::WrapText(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; @@ -447,6 +494,12 @@ int LuaFonts::WrapText(lua_State* L) /******************************************************************************/ /******************************************************************************/ +/*** Returns the horizontal extent of a string for this font at its current size. + * + * @function LuaFont:GetTextWidth + * @param text string + * @return number width + */ int LuaFonts::GetTextWidth(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; @@ -457,6 +510,14 @@ int LuaFonts::GetTextWidth(lua_State* L) } +/*** Returns layout metrics for a string: total height, descender depth, and line count. + * + * @function LuaFont:GetTextHeight + * @param text string + * @return number height + * @return number descender + * @return number lines + */ int LuaFonts::GetTextHeight(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; @@ -508,10 +569,26 @@ static int SetTextColorShared(lua_State* L, bool outline) return 0; } +/*** Sets the RGBA color used when drawing text (fill). + * + * @function LuaFont:SetTextColor + * @param color table Four-component RGBA array (`{r, g, b, a}`), or pass `r`, `g`, `b`, and optional `a` as separate numbers (requires at least three numeric components after the font). + */ int LuaFonts::SetTextColor(lua_State* L) { return (SetTextColorShared(L, false)); } + +/*** Sets the RGBA color used for text outline when outline rendering is enabled. + * + * @function LuaFont:SetOutlineColor + * @param color table Four-component RGBA array (`{r, g, b, a}`), or pass `r`, `g`, `b`, and optional `a` as separate numbers (requires at least three numeric components after the font). + */ int LuaFonts::SetOutlineColor(lua_State* L) { return (SetTextColorShared(L, true)); } +/*** When enabled, outline color is derived automatically instead of using `SetOutlineColor`. + * + * @function LuaFont:SetAutoOutlineColor + * @param enabled boolean + */ int LuaFonts::SetAutoOutlineColor(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; diff --git a/rts/Lua/LuaObjectRendering.cpp b/rts/Lua/LuaObjectRendering.cpp index c26b885be23..ac1f246e04c 100644 --- a/rts/Lua/LuaObjectRendering.cpp +++ b/rts/Lua/LuaObjectRendering.cpp @@ -1,5 +1,16 @@ /* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ + +/*** +Object rendering API — controls LOD, materials, and custom draw for units/features. +Registered as `Spring.UnitRendering` and `Spring.FeatureRendering`. +@see rts/Lua/LuaObjectRendering.cpp +*/ + +/*** +@class ObjectRenderingTable +*/ + #include "LuaObjectRendering.h" #include "LuaMaterial.h" @@ -122,6 +133,13 @@ void LuaObjectRenderingImpl::PushFunction(lua_State* L, int (*fnPntr)(lua_State* +/*** Get the number of LOD levels and the current LOD. + * + * @function ObjectRenderingTable.GetLODCount + * @param objectID integer + * @return integer lodCount + * @return integer currentLOD + */ int LuaObjectRenderingImpl::GetLODCount(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; @@ -137,6 +155,13 @@ int LuaObjectRenderingImpl::GetLODCount(lua_State* L) return 2; } +/*** Set the number of LOD levels. + * + * @function ObjectRenderingTable.SetLODCount + * @param objectID integer + * @param lodCount integer + * @return nil + */ int LuaObjectRenderingImpl::SetLODCount(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; @@ -167,6 +192,14 @@ static int SetLODLengthCommon(lua_State* L, CSolidObject* obj, float scale) return 0; } +/*** Set the LOD transition length for a given level. + * + * @function ObjectRenderingTable.SetLODLength + * @param objectID integer + * @param lodLevel integer + * @param lodLength number + * @return nil + */ int LuaObjectRenderingImpl::SetLODLength(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; @@ -174,6 +207,14 @@ int LuaObjectRenderingImpl::SetLODLength(lua_State* L) return (SetLODLengthCommon(L, ParseSolidObject(L, __func__, 1, GetObjectType()), 1.0f)); } +/*** Set the LOD transition distance for a given level (scaled for 45-degree FOV). + * + * @function ObjectRenderingTable.SetLODDistance + * @param objectID integer + * @param lodLevel integer + * @param lodDistance number + * @return nil + */ int LuaObjectRenderingImpl::SetLODDistance(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; @@ -187,6 +228,15 @@ int LuaObjectRenderingImpl::SetLODDistance(lua_State* L) /******************************************************************************/ +/*** Set a display list for a piece at a given LOD and material. + * + * @function ObjectRenderingTable.SetPieceList + * @param objectID integer + * @param lodLevel integer + * @param piece integer + * @param ... any + * @return nil + */ int LuaObjectRenderingImpl::SetPieceList(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; @@ -334,6 +384,14 @@ static LuaMatRef ParseMaterial(lua_State* L, int index, LuaMatType matType) { /******************************************************************************/ /******************************************************************************/ +/*** Get a material reference for a LOD level. + * + * @function ObjectRenderingTable.GetMaterial + * @param objectID integer + * @param lodLevel integer + * @param materialName string + * @return userdata matRef + */ int LuaObjectRenderingImpl::GetMaterial(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; @@ -356,6 +414,15 @@ int LuaObjectRenderingImpl::GetMaterial(lua_State* L) /******************************************************************************/ /******************************************************************************/ +/*** Set a material for a LOD level. + * + * @function ObjectRenderingTable.SetMaterial + * @param objectID integer + * @param lodLevel integer + * @param materialName string + * @param materialTable table + * @return nil + */ int LuaObjectRenderingImpl::SetMaterial(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; @@ -392,6 +459,14 @@ int LuaObjectRenderingImpl::SetMaterial(lua_State* L) } +/*** Set the last LOD level that uses a given material. + * + * @function ObjectRenderingTable.SetMaterialLastLOD + * @param objectID integer + * @param materialName string + * @param lastLOD integer + * @return nil + */ int LuaObjectRenderingImpl::SetMaterialLastLOD(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; @@ -410,6 +485,15 @@ int LuaObjectRenderingImpl::SetMaterialLastLOD(lua_State* L) return 0; } +/*** Set display lists for a material. + * + * @function ObjectRenderingTable.SetMaterialDisplayLists + * @param objectID integer + * @param lodLevel integer + * @param materialName string + * @param displayListTable table + * @return nil + */ int LuaObjectRenderingImpl::SetMaterialDisplayLists(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; @@ -498,7 +582,17 @@ static int SetMaterialUniform(lua_State* L, LuaObjType objType, LuaMatShader::Pa return 1; } +/*** @function ObjectRenderingTable.SetDeferredMaterialUniform + * @param objectID integer + * @param ... any uniform values + * @return nil + */ int LuaObjectRenderingImpl::SetDeferredMaterialUniform(lua_State* L) { return (SetMaterialUniform(L, GetObjectType(), LuaMatShader::LUASHADER_PASS_DFR)); } +/*** @function ObjectRenderingTable.SetForwardMaterialUniform + * @param objectID integer + * @param ... any uniform values + * @return nil + */ int LuaObjectRenderingImpl::SetForwardMaterialUniform(lua_State* L) { return (SetMaterialUniform(L, GetObjectType(), LuaMatShader::LUASHADER_PASS_FWD)); } @@ -537,7 +631,17 @@ static int ClearMaterialUniform(lua_State* L, LuaObjType objType, LuaMatShader:: return 1; } +/*** @function ObjectRenderingTable.ClearDeferredMaterialUniform + * @param objectID integer + * @param ... any uniform indices + * @return nil + */ int LuaObjectRenderingImpl::ClearDeferredMaterialUniform(lua_State* L) { return (ClearMaterialUniform(L, GetObjectType(), LuaMatShader::LUASHADER_PASS_FWD)); } +/*** @function ObjectRenderingTable.ClearForwardMaterialUniform + * @param objectID integer + * @param ... any uniform indices + * @return nil + */ int LuaObjectRenderingImpl::ClearForwardMaterialUniform(lua_State* L) { return (ClearMaterialUniform(L, GetObjectType(), LuaMatShader::LUASHADER_PASS_DFR)); } @@ -559,18 +663,39 @@ static int SetObjectLuaDraw(lua_State* L, ObjectType* obj) } +/*** Enable or disable custom Lua drawing for a unit. + * + * @function ObjectRenderingTable.SetUnitLuaDraw + * @param unitID integer + * @param enable boolean + * @return nil + */ int LuaObjectRenderingImpl::SetUnitLuaDraw(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; return (SetObjectLuaDraw(L, unitHandler.GetUnit(luaL_checkint(L, 1)))); } +/*** Enable or disable custom Lua drawing for a feature. + * + * @function ObjectRenderingTable.SetFeatureLuaDraw + * @param featureID integer + * @param enable boolean + * @return nil + */ int LuaObjectRenderingImpl::SetFeatureLuaDraw(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; return (SetObjectLuaDraw(L, featureHandler.GetFeature(luaL_checkint(L, 1)))); } +/*** Enable or disable custom Lua drawing for a projectile. + * + * @function ObjectRenderingTable.SetProjectileLuaDraw + * @param projectileID integer + * @param enable boolean + * @return nil + */ int LuaObjectRenderingImpl::SetProjectileLuaDraw(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; @@ -602,6 +727,12 @@ static void PrintObjectLOD(const CSolidObject* obj, int lod) } +/*** Print debug info about the object's material data. + * + * @function ObjectRenderingTable.Debug + * @param objectID integer + * @return nil + */ int LuaObjectRenderingImpl::Debug(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; diff --git a/rts/Lua/LuaOpenGL.cpp b/rts/Lua/LuaOpenGL.cpp index 6c81432df53..a01bb055197 100644 --- a/rts/Lua/LuaOpenGL.cpp +++ b/rts/Lua/LuaOpenGL.cpp @@ -1369,7 +1369,7 @@ int LuaOpenGL::EndText(lua_State* L) * @param text string * @param x number * @param y number - * @param size number + * @param size number? * @param options string? concatenated string of option characters. * * - horizontal alignment: @@ -2065,8 +2065,8 @@ int LuaOpenGL::DrawFuncAtUnit(lua_State* L) * @param radius number * @param resolution integer * @param slope number - * @param gravity number - * @param weaponDefID integer + * @param gravity number? + * @param weaponDefID integer? */ int LuaOpenGL::DrawGroundCircle(lua_State* L) { @@ -2901,7 +2901,7 @@ int LuaOpenGL::TexRect(lua_State* L) /*** * @function gl.DispatchCompute - * @param numGroupX integer + * @param numGroupX integer? * @param numGroupY integer * @param numGroupZ integer * @param barriers integer? (Default: `0`) @@ -4538,7 +4538,7 @@ int LuaOpenGL::TexEnv(lua_State* L) * @param texNum integer * @param target GL * @param pname GL - * @param value number + * @param value number? */ /*** * @function gl.MultiTexEnv @@ -4609,7 +4609,7 @@ static void SetTexGenState(GLenum target, bool state) * @function gl.TexGen * @param target GL * @param pname GL - * @param value number + * @param value number? */ /*** * @function gl.TexGen @@ -4669,7 +4669,7 @@ int LuaOpenGL::TexGen(lua_State* L) * @param texNum integer * @param target GL * @param pname GL - * @param value number + * @param value number? */ /*** * @function gl.MultiTexGen @@ -5073,6 +5073,10 @@ int LuaOpenGL::GetEngineAtlasTextures(lua_State* L) { /******************************************************************************/ +/*** + * @function gl.Clear + * @param bits GL any buffer bit mask (e.g. `GL.STENCIL_BUFFER_BIT`). Clears with current/default values. + */ /*** * @function gl.Clear * @param bits GL `GL.DEPTH_BUFFER_BIT` or `GL.STENCIL_BUFFER_BIT`. @@ -6397,10 +6401,10 @@ int LuaOpenGL::ReadPixels(lua_State* L) /*** * @class SaveImageOptions * @x_helper - * @field alpha boolean (Default: `false`) - * @field yflip boolean (Default: `true`) - * @field grayscale16bit boolean (Default: `false`) - * @field readbuffer GL (Default: current read buffer) + * @field alpha boolean? (Default: `false`) + * @field yflip boolean? (Default: `true`) + * @field grayscale16bit boolean? (Default: `false`) + * @field readbuffer GL? (Default: current read buffer) */ /*** @@ -7081,7 +7085,7 @@ int LuaOpenGL::ObjectLabel(lua_State* L) { * @function gl.PushDebugGroup * @param id integer A numeric identifier for the group, can be any unique number. * @param message string A human-readable string describing the debug group. Will be truncated if longer than driver-specific limit - * @param sourceIsThirdParty boolean Set the source tag, true for GL_DEBUG_SOURCE_THIRD_PARTY, false for GL_DEBUG_SOURCE_APPLICATION. default false + * @param sourceIsThirdParty boolean? Set the source tag, true for GL_DEBUG_SOURCE_THIRD_PARTY, false for GL_DEBUG_SOURCE_APPLICATION. default false * @return nil */ int LuaOpenGL::PushDebugGroup(lua_State* L) { diff --git a/rts/Lua/LuaPathFinder.cpp b/rts/Lua/LuaPathFinder.cpp index 9e9c3dc24b8..3184bca2510 100644 --- a/rts/Lua/LuaPathFinder.cpp +++ b/rts/Lua/LuaPathFinder.cpp @@ -211,6 +211,18 @@ static void CreatePathMetatable(lua_State* L) /******************************************************************************/ /******************************************************************************/ +/*** + * @function Spring.RequestPath + * @param moveID number|string + * @param startX number + * @param startY number + * @param startZ number + * @param endX number + * @param endY number + * @param endZ number + * @param radius number? + * @return userdata? path + */ int LuaPathFinder::RequestPath(lua_State* L) { const MoveDef* moveDef = nullptr; @@ -250,6 +262,13 @@ int LuaPathFinder::RequestPath(lua_State* L) +/*** + * @function Spring.InitPathNodeCostsArray + * @param overlayIndex number + * @param sizeX number + * @param sizeZ number + * @return boolean success + */ int LuaPathFinder::InitPathNodeCostsArray(lua_State* L) { const unsigned int overlayIndex = luaL_checkint(L, 1); @@ -282,6 +301,11 @@ int LuaPathFinder::InitPathNodeCostsArray(lua_State* L) return 1; } +/*** + * @function Spring.FreePathNodeCostsArray + * @param overlayIndex number + * @return boolean success + */ int LuaPathFinder::FreePathNodeCostsArray(lua_State* L) { const unsigned int overlayIndex = luaL_checkint(L, 1); @@ -315,6 +339,11 @@ int LuaPathFinder::FreePathNodeCostsArray(lua_State* L) +/*** + * @function Spring.SetPathNodeCosts + * @param overlayIndex number + * @return boolean success + */ int LuaPathFinder::SetPathNodeCosts(lua_State* L) { const unsigned int overlayIndex = luaL_checkint(L, 1); @@ -339,6 +368,11 @@ int LuaPathFinder::SetPathNodeCosts(lua_State* L) return 1; } +/*** + * @function Spring.GetPathNodeCosts + * @param overlayIndex number + * @return boolean|table costs + */ int LuaPathFinder::GetPathNodeCosts(lua_State* L) { const unsigned int overlayIndex = luaL_checkint(L, 1); @@ -370,6 +404,14 @@ int LuaPathFinder::GetPathNodeCosts(lua_State* L) +/*** + * @function Spring.SetPathNodeCost + * @param overlayIndex number + * @param nodeX number + * @param nodeZ number + * @param cost number + * @return boolean success + */ int LuaPathFinder::SetPathNodeCost(lua_State* L) { const unsigned int overlayIndex = luaL_checkint(L, 1); @@ -400,6 +442,12 @@ int LuaPathFinder::SetPathNodeCost(lua_State* L) return 1; } +/*** + * @function Spring.GetPathNodeCost + * @param nodeX number + * @param nodeZ number + * @return number cost + */ int LuaPathFinder::GetPathNodeCost(lua_State* L) { const unsigned int hmx = luaL_checkint(L, 1); diff --git a/rts/Lua/LuaRules.cpp b/rts/Lua/LuaRules.cpp index 19b5ac69707..0c1a1ace8b0 100644 --- a/rts/Lua/LuaRules.cpp +++ b/rts/Lua/LuaRules.cpp @@ -70,6 +70,7 @@ int CLuaRules::GetInitSelectTeam() const } + /****************************************************************************** * Lua Rules * @@ -91,11 +92,13 @@ bool CLuaRules::AddUnsyncedCode(lua_State* L) { lua_getglobal(L, "Spring"); + /*** @field Spring.UnitRendering ObjectRenderingTable */ lua_pushliteral(L, "UnitRendering"); lua_createtable(L, 0, 17); LuaObjectRendering::PushEntries(L); lua_rawset(L, -3); + /*** @field Spring.FeatureRendering ObjectRenderingTable */ lua_pushliteral(L, "FeatureRendering"); lua_createtable(L, 0, 17); LuaObjectRendering::PushEntries(L); diff --git a/rts/Lua/LuaSyncedCtrl.cpp b/rts/Lua/LuaSyncedCtrl.cpp index 6f0a8941d8f..fe7eeea65e5 100644 --- a/rts/Lua/LuaSyncedCtrl.cpp +++ b/rts/Lua/LuaSyncedCtrl.cpp @@ -391,6 +391,7 @@ bool LuaSyncedCtrl::PushEntries(lua_State* L) if (!LuaSyncedMoveCtrl::PushMoveCtrl(L)) return false; + /*** @field Spring.UnitScript UnitScriptTable */ if (!CLuaUnitScript::PushEntries(L)) return false; @@ -1161,7 +1162,7 @@ int LuaSyncedCtrl::GameOver(lua_State* L) /*** Set tidal strength * * @function Spring.SetTidal - * @param strength number + * @param strength number? * @return nil */ int LuaSyncedCtrl::SetTidal(lua_State* L) @@ -1174,8 +1175,8 @@ int LuaSyncedCtrl::SetTidal(lua_State* L) /*** Set wind strength * * @function Spring.SetWind - * @param minStrength number - * @param maxStrength number + * @param minStrength number? + * @param maxStrength number? * @return nil */ int LuaSyncedCtrl::SetWind(lua_State* L) @@ -1613,7 +1614,7 @@ void SetRulesParam(lua_State* L, const char* caller, int offset, /*** * @function Spring.SetGameRulesParam * @param paramName string - * @param paramValue ?number|string numeric paramValues in quotes will be converted to number. + * @param paramValue (number|string|boolean)? numeric paramValues in quotes will be converted to number. * @param losAccess losAccess? * @return nil */ @@ -1628,7 +1629,7 @@ int LuaSyncedCtrl::SetGameRulesParam(lua_State* L) * @function Spring.SetTeamRulesParam * @param teamID integer * @param paramName string - * @param paramValue ?number|string numeric paramValues in quotes will be converted to number. + * @param paramValue (number|string|boolean)? numeric paramValues in quotes will be converted to number. * @param losAccess losAccess? * @return nil */ @@ -1646,7 +1647,7 @@ int LuaSyncedCtrl::SetTeamRulesParam(lua_State* L) * @function Spring.SetPlayerRulesParam * @param playerID integer * @param paramName string - * @param paramValue ?number|string numeric paramValues in quotes will be converted to number. + * @param paramValue (number|string|boolean)? numeric paramValues in quotes will be converted to number. * @param losAccess losAccess? * @return nil */ @@ -1670,7 +1671,7 @@ int LuaSyncedCtrl::SetPlayerRulesParam(lua_State* L) * @function Spring.SetUnitRulesParam * @param unitID integer * @param paramName string - * @param paramValue ?number|string numeric paramValues in quotes will be converted to number. + * @param paramValue (number|string|boolean)? numeric paramValues in quotes will be converted to number. * @param losAccess losAccess? * @return nil */ @@ -1690,7 +1691,7 @@ int LuaSyncedCtrl::SetUnitRulesParam(lua_State* L) * @function Spring.SetFeatureRulesParam * @param featureID integer * @param paramName string - * @param paramValue ?number|string numeric paramValues in quotes will be converted to number. + * @param paramValue (number|string|boolean)? numeric paramValues in quotes will be converted to number. * @param losAccess losAccess? * @return nil */ @@ -1860,7 +1861,7 @@ int LuaSyncedCtrl::GetCOBScriptID(lua_State* L) * @param posY number * @param posZ number * @param facing Facing - * @param teamID integer + * @param teamID integer? * @param build boolean? (Default: `false`) The unit is created in "being built" state with zero `buildProgress`. * @param flattenGround boolean? (Default: `true`) The unit flattens ground, if it normally does so. * @param unitID integer? Request a specific unitID. @@ -3023,8 +3024,8 @@ int LuaSyncedCtrl::SetUnitLosState(lua_State* L) * - if the boolean is true it takes the absolute value of it. * * @param unitID integer - * @param cloak boolean|number - * @param cloakArg boolean|number + * @param cloak (boolean|number)? + * @param cloakArg (boolean|number)? * @return nil */ int LuaSyncedCtrl::SetUnitCloak(lua_State* L) @@ -3186,7 +3187,7 @@ int LuaSyncedCtrl::SetUnitMetalExtraction(lua_State* L) * * @function Spring.SetUnitHarvestStorage * @param unitID integer - * @param metal number + * @param metal number? * @return nil */ int LuaSyncedCtrl::SetUnitHarvestStorage(lua_State* L) @@ -3208,7 +3209,7 @@ int LuaSyncedCtrl::SetUnitHarvestStorage(lua_State* L) * @function Spring.SetUnitBuildParams * @param unitID integer * @param paramName string one of `buildRange`|`buildDistance`|`buildRange3D` - * @param value number|boolean boolean when `paramName` is `buildRange3D`, otherwise number. + * @param value (number|boolean)? boolean when `paramName` is `buildRange3D`, otherwise number. * @return nil */ int LuaSyncedCtrl::SetUnitBuildParams(lua_State* L) @@ -3369,7 +3370,7 @@ int LuaSyncedCtrl::SetUnitBlocking(lua_State* L) /*** * @function Spring.SetUnitCrashing * @param unitID integer - * @param crashing boolean + * @param crashing boolean? * @return boolean success */ int LuaSyncedCtrl::SetUnitCrashing(lua_State* L) { @@ -3592,7 +3593,8 @@ int LuaSyncedCtrl::SetUnitNeutral(lua_State* L) * @param enemyUnitID integer? when nil drops the units current target. * @param dgun boolean? (Default: `false`) * @param userTarget boolean? (Default: `false`) - * @param weaponNum number? (Default: `-1`) + * @param dontForceTarget boolean? + * @param weaponNum integer? (Default: `-1`) * @return boolean success */ @@ -3604,7 +3606,8 @@ int LuaSyncedCtrl::SetUnitNeutral(lua_State* L) * @param z number? * @param dgun boolean? (Default: `false`) * @param userTarget boolean? (Default: `false`) - * @param weaponNum number? (Default: `-1`) + * @param dontForceTarget boolean? + * @param weaponNum integer? (Default: `-1`) * @return boolean success */ int LuaSyncedCtrl::SetUnitTarget(lua_State* L) @@ -3716,8 +3719,8 @@ int LuaSyncedCtrl::SetUnitMidAndAimPos(lua_State* L) /*** * @function Spring.SetUnitRadiusAndHeight * @param unitID integer - * @param radius number - * @param height number + * @param radius number? + * @param height number? * @return boolean success */ int LuaSyncedCtrl::SetUnitRadiusAndHeight(lua_State* L) @@ -3961,12 +3964,12 @@ int LuaSyncedCtrl::SetUnitSensorRadius(lua_State* L) * * @function Spring.SetUnitPosErrorParams * @param unitID integer - * @param posErrorVectorX number - * @param posErrorVectorY number - * @param posErrorVectorZ number - * @param posErrorDeltaX number - * @param posErrorDeltaY number - * @param posErrorDeltaZ number + * @param posErrorVectorX number? + * @param posErrorVectorY number? + * @param posErrorVectorZ number? + * @param posErrorDeltaX number? + * @param posErrorDeltaY number? + * @param posErrorDeltaZ number? * @param nextPosErrorUpdate number? * @return nil */ @@ -4062,6 +4065,7 @@ int LuaSyncedCtrl::SetUnitLandGoal(lua_State* L) /*** * @function Spring.ClearUnitGoal * @param unitID integer + * @param maneuver boolean? * @return nil */ int LuaSyncedCtrl::ClearUnitGoal(lua_State* L) @@ -4936,7 +4940,7 @@ int LuaSyncedCtrl::SetFeatureResources(lua_State* L) * @function Spring.SetFeatureResurrect * * @param featureID integer - * @param unitDef string|integer Can be a number id or a string name, this allows cancelling resurrection by passing `-1`. + * @param unitDef (string|integer)? Can be a number id or a string name, this allows cancelling resurrection by passing `-1`. * @param facing Facing? (Default: `"south"`) * @param progress number? Set the level of progress. * @return nil @@ -5277,8 +5281,8 @@ int LuaSyncedCtrl::SetFeatureMidAndAimPos(lua_State* L) /*** * @function Spring.SetFeatureRadiusAndHeight * @param featureID integer - * @param radius number - * @param height number + * @param radius number? + * @param height number? * @return boolean success */ int LuaSyncedCtrl::SetFeatureRadiusAndHeight(lua_State* L) @@ -5555,7 +5559,7 @@ int LuaSyncedCtrl::SetProjectileUseAirLos(lua_State* L) * @function Spring.SetProjectileMoveControl * * @param projectileID integer - * @param enable boolean + * @param enable boolean? */ int LuaSyncedCtrl::SetProjectileMoveControl(lua_State* L) { @@ -6533,7 +6537,6 @@ int LuaSyncedCtrl::SetHeightMap(lua_State* L) * ``` * * @param luaFunction function - * @param arg number * @param ... number * @return integer? absTotalHeightMapAmountChanged */ @@ -7296,7 +7299,7 @@ int LuaSyncedCtrl::ForceUnitCollisionUpdate(lua_State* L) * @param transporterID integer * @param passengerID integer * @param pieceNum number - * @param force boolean + * @param force boolean? * @return nil */ int LuaSyncedCtrl::UnitAttach(lua_State* L) @@ -7418,21 +7421,21 @@ int LuaSyncedCtrl::SetUnitLoadingTransport(lua_State* L) /*** * @class ProjectileParams * @x_helper - * @field pos xyz - * @field speed xyz - * @field spread xyz - * @field error xyz - * @field end xyz - * @field owner integer - * @field team integer - * @field ttl number - * @field gravity number - * @field tracking number - * @field maxRange number - * @field startAlpha number - * @field endAlpha number - * @field model string - * @field cegTag string + * @field pos xyz? + * @field speed xyz? + * @field spread xyz? + * @field error xyz? + * @field end xyz? + * @field owner integer? + * @field team integer? + * @field ttl number? + * @field gravity number? + * @field tracking number? + * @field maxRange number? + * @field startAlpha number? + * @field endAlpha number? + * @field model string? + * @field cegTag string? */ /*** @@ -7594,18 +7597,18 @@ static int SetExplosionParam(lua_State* L, CExplosionParams& params, DamageArray * * @class ExplosionParams * @x_helper - * @field weaponDef number - * @field owner number - * @field hitUnit number - * @field hitFeature number - * @field craterAreaOfEffect number - * @field damageAreaOfEffect number - * @field edgeEffectiveness number - * @field explosionSpeed number - * @field gfxMod number - * @field impactOnly boolean - * @field ignoreOwner boolean - * @field damageGround boolean + * @field weaponDef number? + * @field owner number? + * @field hitUnit number? + * @field hitFeature number? + * @field craterAreaOfEffect number? + * @field damageAreaOfEffect number? + * @field edgeEffectiveness number? + * @field explosionSpeed number? + * @field gfxMod number? + * @field impactOnly boolean? + * @field ignoreOwner boolean? + * @field damageGround boolean? */ /*** @@ -7616,7 +7619,7 @@ static int SetExplosionParam(lua_State* L, CExplosionParams& params, DamageArray * @param dirX number? (Default: `0`) * @param dirY number? (Default: `0`) * @param dirZ number? (Default: `0`) - * @param explosionParams ExplosionParams + * @param explosionParams ExplosionParams? * @return nil */ int LuaSyncedCtrl::SpawnExplosion(lua_State* L) diff --git a/rts/Lua/LuaSyncedRead.cpp b/rts/Lua/LuaSyncedRead.cpp index 1af92101752..65c2cd10400 100644 --- a/rts/Lua/LuaSyncedRead.cpp +++ b/rts/Lua/LuaSyncedRead.cpp @@ -91,6 +91,7 @@ using std::max; static const LuaHashString hs_n("n"); + /****************************************************************************** * Synced Read * @@ -99,10 +100,13 @@ static const LuaHashString hs_n("n"); bool LuaSyncedRead::PushEntries(lua_State* L) { - // allegiance constants + /*** @field Spring.ALL_UNITS number */ LuaPushNamedNumber(L, "ALL_UNITS", LuaUtils::AllUnits); + /*** @field Spring.MY_UNITS number */ LuaPushNamedNumber(L, "MY_UNITS", LuaUtils::MyUnits); + /*** @field Spring.ALLY_UNITS number */ LuaPushNamedNumber(L, "ALLY_UNITS", LuaUtils::AllyUnits); + /*** @field Spring.ENEMY_UNITS number */ LuaPushNamedNumber(L, "ENEMY_UNITS", LuaUtils::EnemyUnits); // READ routines, sync safe @@ -767,7 +771,6 @@ static int GetRulesParam(lua_State* L, const char* caller, int index, * @section gamestates ******************************************************************************/ - /*** * * @function Spring.IsCheatingEnabled @@ -1156,9 +1159,9 @@ int LuaSyncedRead::GetFeatureRulesParams(lua_State* L) * * @function Spring.GetGameRulesParam * - * @param ruleRef number|string the rule index or name + * @param name string rules-parameter key (only this argument is read) * - * @return number?|string value + * @return number|boolean|string|nil value */ int LuaSyncedRead::GetGameRulesParam(lua_State* L) { @@ -3072,6 +3075,7 @@ int LuaSyncedRead::GetUnitsInBox(lua_State* L) * @param x number * @param z number * @param radius number + * @param teamID integer? filter by team, defaults to all units * @return number[] unitIDs */ int LuaSyncedRead::GetUnitsInCylinder(lua_State* L) @@ -3683,6 +3687,9 @@ int LuaSyncedRead::ValidUnitID(lua_State* L) * * @function Spring.GetUnitStates * @param unitID integer + * @param retTable boolean? + * @param binState boolean? + * @param amtState boolean? * @return UnitState */ int LuaSyncedRead::GetUnitStates(lua_State* L) @@ -4053,8 +4060,8 @@ int LuaSyncedRead::GetUnitDefID(lua_State* L) * * @param unitID integer * -* @return integer|boolean|nil moveDefID -* @return string? moveDefName +* @return integer|false|nil moveDefID +* @return string|nil moveDefName */ int LuaSyncedRead::GetUnitMoveDefID(lua_State* L) @@ -5140,7 +5147,7 @@ int LuaSyncedRead::GetUnitMaxRange(lua_State* L) * * @param unitID integer * @param weaponNum number - * @param stateName string + * @param stateName string? * @return number stateValue */ int LuaSyncedRead::GetUnitWeaponState(lua_State* L) @@ -5364,6 +5371,13 @@ int LuaSyncedRead::GetUnitWeaponDamages(lua_State* L) * * @function Spring.GetUnitWeaponVectors * @param unitID integer + * @param weaponNum integer 1-indexed weapon number + * @return number? posX + * @return number posY + * @return number posZ + * @return number dirX + * @return number dirY + * @return number dirZ */ int LuaSyncedRead::GetUnitWeaponVectors(lua_State* L) { @@ -5404,6 +5418,19 @@ int LuaSyncedRead::GetUnitWeaponVectors(lua_State* L) * * @function Spring.GetUnitWeaponTryTarget * @param unitID integer + * @param weaponNum integer + * @param targetID integer + * @return boolean canTarget + */ +/*** + * + * @function Spring.GetUnitWeaponTryTarget + * @param unitID integer + * @param weaponNum integer + * @param posX number + * @param posY number + * @param posZ number + * @return boolean canTarget */ int LuaSyncedRead::GetUnitWeaponTryTarget(lua_State* L) { @@ -5449,6 +5476,19 @@ int LuaSyncedRead::GetUnitWeaponTryTarget(lua_State* L) * * @function Spring.GetUnitWeaponTestTarget * @param unitID integer + * @param weaponID integer weapon number (1-based Lua index) + * @param targetUnitID integer enemy unit to test (when fewer than five arguments) + * @return boolean validTarget + */ +/*** + * + * @function Spring.GetUnitWeaponTestTarget + * @param unitID integer + * @param weaponID integer weapon number (1-based Lua index) + * @param targetX number world X to test (with `targetY`, `targetZ`; used when at least five arguments are passed) + * @param targetY number + * @param targetZ number + * @return boolean validTarget */ int LuaSyncedRead::GetUnitWeaponTestTarget(lua_State* L) { @@ -5487,6 +5527,19 @@ int LuaSyncedRead::GetUnitWeaponTestTarget(lua_State* L) * * @function Spring.GetUnitWeaponTestRange * @param unitID integer + * @param weaponNum integer + * @param targetID integer + * @return boolean inRange + */ +/*** + * + * @function Spring.GetUnitWeaponTestRange + * @param unitID integer + * @param weaponNum integer + * @param posX number + * @param posY number + * @param posZ number + * @return boolean inRange */ int LuaSyncedRead::GetUnitWeaponTestRange(lua_State* L) { @@ -5525,6 +5578,43 @@ int LuaSyncedRead::GetUnitWeaponTestRange(lua_State* L) * * @function Spring.GetUnitWeaponHaveFreeLineOfFire * @param unitID integer + * @param weaponNum integer + * @param targetID integer + * @return boolean haveFreeLineOfFire + */ +/*** + * + * @function Spring.GetUnitWeaponHaveFreeLineOfFire + * @param unitID integer + * @param weaponNum integer + * @param srcPosX number + * @param srcPosY number + * @param srcPosZ number + * @return boolean haveFreeLineOfFire + */ +/*** + * + * @function Spring.GetUnitWeaponHaveFreeLineOfFire + * @param unitID integer + * @param weaponNum integer + * @param srcPosX number + * @param srcPosY number + * @param srcPosZ number + * @param targetID integer + * @return boolean haveFreeLineOfFire + */ +/*** + * + * @function Spring.GetUnitWeaponHaveFreeLineOfFire + * @param unitID integer + * @param weaponNum integer + * @param srcPosX number + * @param srcPosY number + * @param srcPosZ number + * @param tgtPosX number + * @param tgtPosY number + * @param tgtPosZ number + * @return boolean haveFreeLineOfFire */ int LuaSyncedRead::GetUnitWeaponHaveFreeLineOfFire(lua_State* L) { @@ -5591,6 +5681,11 @@ int LuaSyncedRead::GetUnitWeaponHaveFreeLineOfFire(lua_State* L) * * @function Spring.GetUnitWeaponCanFire * @param unitID integer + * @param weaponNum integer + * @param ignoreAngleGood boolean? + * @param ignoreTargetType boolean? + * @param ignoreRequestedDir boolean? + * @return boolean canFire */ int LuaSyncedRead::GetUnitWeaponCanFire(lua_State* L) { @@ -5844,6 +5939,9 @@ int LuaSyncedRead::GetUnitSeparation(lua_State* L) * * @function Spring.GetUnitFeatureSeparation * @param unitID integer + * @param featureID integer + * @param flat boolean? (Default: `false`) if true, XZ (2D) distance; otherwise 3D distance + * @return number distance */ int LuaSyncedRead::GetUnitFeatureSeparation(lua_State* L) { @@ -7793,6 +7891,12 @@ static void ParseMapCoords(lua_State* L, const char* caller, /*** * * @function Spring.GetGroundBlocked + * @param x number world x coordinate (or xMin when using 4-arg form) + * @param z number world z coordinate (or zMin when using 4-arg form) + * @param x2 number? world xMax (4-arg rectangle form) + * @param z2 number? world zMax (4-arg rectangle form) + * @return string? objectType `"feature"` or `"unit"` + * @return number? objectID the feature or unit ID */ int LuaSyncedRead::GetGroundBlocked(lua_State* L) { @@ -8260,7 +8364,7 @@ int LuaSyncedRead::IsPosInAirLos(lua_State* L) * @function Spring.GetUnitLosState * @param unitID integer * @param allyTeamID integer? - * @param raw false? Return a table. + * @param raw false? (Default: `false`) Return a table. * @return table<"los"|"radar"|"typed",boolean>? los A table of LOS state names as keys and booleans as values, or `nil` if `unitID` is invalid. */ int LuaSyncedRead::GetUnitLosState(lua_State* L) @@ -8309,7 +8413,7 @@ int LuaSyncedRead::GetUnitLosState(lua_State* L) * * @function Spring.IsUnitInLos * @param unitID integer - * @param allyTeamID integer + * @param allyTeamID integer? defaults to the calling widget/gadget's ally team * @return boolean inLos */ int LuaSyncedRead::IsUnitInLos(lua_State* L) @@ -8333,7 +8437,7 @@ int LuaSyncedRead::IsUnitInLos(lua_State* L) * * @function Spring.IsUnitInAirLos * @param unitID integer - * @param allyTeamID integer + * @param allyTeamID integer? defaults to the calling widget/gadget's ally team * @return boolean inAirLos */ int LuaSyncedRead::IsUnitInAirLos(lua_State* L) @@ -8357,7 +8461,7 @@ int LuaSyncedRead::IsUnitInAirLos(lua_State* L) * * @function Spring.IsUnitInRadar * @param unitID integer - * @param allyTeamID integer + * @param allyTeamID integer? defaults to the calling widget/gadget's ally team * @return boolean inRadar */ int LuaSyncedRead::IsUnitInRadar(lua_State* L) diff --git a/rts/Lua/LuaUI.cpp b/rts/Lua/LuaUI.cpp index 46f2bee87c6..002184fd1df 100644 --- a/rts/Lua/LuaUI.cpp +++ b/rts/Lua/LuaUI.cpp @@ -673,6 +673,12 @@ bool CLuaUI::GetLuaCmdDescList(lua_State* L, int index, vectorhaveShockFront = true; diff --git a/rts/Lua/LuaUnsyncedCtrl.cpp b/rts/Lua/LuaUnsyncedCtrl.cpp index 2079d9724ce..d140eab26f4 100644 --- a/rts/Lua/LuaUnsyncedCtrl.cpp +++ b/rts/Lua/LuaUnsyncedCtrl.cpp @@ -115,6 +115,7 @@ #undef Yield + /****************************************************************************** * Callouts to set state * @@ -467,12 +468,11 @@ static inline CUnit* ParseSelectUnit(lua_State* L, const char* caller, int index * @section console ******************************************************************************/ - /*** Send a ping request to the server * * @function Spring.Ping * - * @param pingTag number + * @param pingTag number? * * @return nil */ @@ -798,14 +798,14 @@ int LuaUnsyncedCtrl::LoadSoundDef(lua_State* L) /*** @function Spring.PlaySoundFile * @param soundfile string - * @param volume number? (Default: 1.0) - * @param posx number? + * @param volume number? (Default: 1.0) optional; all following arguments are optional + * @param posx number? world position X (use with `posy` and `posz`, or omit all three) * @param posy number? * @param posz number? - * @param speedx number? + * @param speedx number? velocity X (use with `speedy` and `speedz` after position, or omit all three) * @param speedy number? * @param speedz number? - * @param channel SoundChannel? (Default: `0|"general"`) + * @param channel SoundChannel? (Default: `0|"general"`) optional; parsed from the last argument index after position and speed triples * @return boolean playSound */ int LuaUnsyncedCtrl::PlaySoundFile(lua_State* L) @@ -1212,7 +1212,10 @@ static CCameraController::StateMap ParseCamStateMap(lua_State* L, int tableIdx) * @param x number * @param y number * @param z number - * @param transTime number? + * @param transTime number? (Default: `0.5`) transition duration; values below zero are clamped to zero + * @param dirX number? (Default: current camera direction X) + * @param dirY number? (Default: current camera direction Y) + * @param dirZ number? (Default: current camera direction Z) * @return nil */ int LuaUnsyncedCtrl::SetCameraTarget(lua_State* L) @@ -1330,7 +1333,7 @@ int LuaUnsyncedCtrl::RunDollyCamera(lua_State* L) /*** Pause Dolly Camera * * @function Spring.PauseDollyCamera - * @param fraction number Fraction of the total runtime to pause at, 0 to 1 inclusive. A null value pauses at current percent + * @param fraction number? Fraction of the total runtime to pause at, 0 to 1 inclusive. A null value pauses at current percent * @return nil */ int LuaUnsyncedCtrl::PauseDollyCamera(lua_State* L) @@ -1922,8 +1925,8 @@ static bool AddLightTrackingTarget(lua_State* L, GL::Light* light, bool trackEna * * @param lightHandle number * @param unitOrProjectileID integer - * @param enableTracking boolean - * @param unitOrProjectile boolean + * @param enableTracking boolean? + * @param unitOrProjectile boolean? * @return boolean success */ int LuaUnsyncedCtrl::SetMapLightTrackingState(lua_State* L) @@ -1958,8 +1961,8 @@ int LuaUnsyncedCtrl::SetMapLightTrackingState(lua_State* L) * * @param lightHandle number * @param unitOrProjectileID integer - * @param enableTracking boolean - * @param unitOrProjectile boolean + * @param enableTracking boolean? + * @param unitOrProjectile boolean? * @return boolean success */ int LuaUnsyncedCtrl::SetModelLightTrackingState(lua_State* L) @@ -2340,7 +2343,6 @@ int LuaUnsyncedCtrl::SetUnitLeaveTracks(lua_State* L) * * @function Spring.SetUnitSelectionVolumeData * @param unitID integer - * @param featureID integer * @param scaleX number * @param scaleY number * @param scaleZ number @@ -2663,7 +2665,7 @@ int LuaUnsyncedCtrl::SetUnitDefIcon(lua_State* L) * @function Spring.SetUnitDefImage * * @param unitDefID integer - * @param image string luaTexture|texFile + * @param image string? luaTexture|texFile * * @return nil */ @@ -2965,6 +2967,7 @@ int LuaUnsyncedCtrl::SetBoxSelectionByEngine(lua_State* L) * @param r number * @param g number * @param b number + * @param alpha number? * @return nil */ int LuaUnsyncedCtrl::SetTeamColor(lua_State* L) @@ -3529,7 +3532,7 @@ static bool CanGiveOrders(const lua_State* L) * * @function Spring.GiveOrder * @param cmdID CMD|integer The command ID. - * @param params CreateCommandParams Parameters for the given command. + * @param params CreateCommandParams? Parameters for the given command. * @param options CreateCommandOptions? * @param timeout integer? Absolute frame number. The command will be discarded after this frame. Only respected by mobile units. * @return boolean @@ -3798,7 +3801,7 @@ int LuaUnsyncedCtrl::SetBuildFacing(lua_State* L) /*** @function Spring.SendLuaUIMsg * @param message string - * @param mode string "s"/"specs" | "a"/"allies" + * @param mode string? "s"/"specs" | "a"/"allies" * @return nil */ int LuaUnsyncedCtrl::SendLuaUIMsg(lua_State* L) @@ -4108,12 +4111,12 @@ int LuaUnsyncedCtrl::MarkerErasePosition(lua_State* L) /*** * @class AtmosphereParams * @x_helper - * @field fogStart number - * @field fogEnd number - * @field sunColor rgba - * @field skyColor rgba - * @field cloudColor rgba - * @field skyAxisAngle xyzw rotation axis and angle in radians of skybox orientation + * @field fogStart number? + * @field fogEnd number? + * @field sunColor rgba? + * @field skyColor rgba? + * @field cloudColor rgba? + * @field skyAxisAngle xyzw? rotation axis and angle in radians of skybox orientation */ /*** Set atmosphere parameters @@ -4253,11 +4256,11 @@ int LuaUnsyncedCtrl::SetSunLighting(lua_State* L) * * @class MapRenderingParams * @x_helper - * @field splatTexMults rgba - * @field splatTexScales rgba - * @field voidWater boolean - * @field voidGround boolean - * @field splatDetailNormalDiffuseAlpha boolean + * @field splatTexMults rgba? + * @field splatTexScales rgba? + * @field voidWater boolean? + * @field voidGround boolean? + * @field splatDetailNormalDiffuseAlpha boolean? */ @@ -4571,44 +4574,44 @@ int LuaUnsyncedCtrl::SetVideoCapturingTimeOffset(lua_State* L) * * @class WaterParams * @x_helper - * @field absorb rgb - * @field baseColor rgb - * @field minColor rgb - * @field surfaceColor rgb - * @field diffuseColor rgb - * @field specularColor rgb - * @field planeColor rgb - * @field texture string file - * @field foamTexture string file - * @field normalTexture string file - * @field damage number - * @field repeatX number - * @field repeatY number - * @field surfaceAlpha number - * @field ambientFactor number - * @field diffuseFactor number - * @field specularFactor number - * @field specularPower number - * @field fresnelMin number - * @field fresnelMax number - * @field fresnelPower number - * @field reflectionDistortion number - * @field blurBase number - * @field blurExponent number - * @field perlinStartFreq number - * @field perlinLacunarity number - * @field perlinAmplitude number - * @field windSpeed number - * @field waveOffsetFactor number - * @field waveLength number - * @field waveFoamDistortion number - * @field waveFoamIntensity number - * @field causticsResolution number - * @field causticsStrength number - * @field numTiles integer - * @field shoreWaves boolean - * @field forceRendering boolean - * @field hasWaterPlane boolean + * @field absorb rgb? + * @field baseColor rgb? + * @field minColor rgb? + * @field surfaceColor rgb? + * @field diffuseColor rgb? + * @field specularColor rgb? + * @field planeColor rgb? + * @field texture string? file + * @field foamTexture string? file + * @field normalTexture string? file + * @field damage number? + * @field repeatX number? + * @field repeatY number? + * @field surfaceAlpha number? + * @field ambientFactor number? + * @field diffuseFactor number? + * @field specularFactor number? + * @field specularPower number? + * @field fresnelMin number? + * @field fresnelMax number? + * @field fresnelPower number? + * @field reflectionDistortion number? + * @field blurBase number? + * @field blurExponent number? + * @field perlinStartFreq number? + * @field perlinLacunarity number? + * @field perlinAmplitude number? + * @field windSpeed number? + * @field waveOffsetFactor number? + * @field waveLength number? + * @field waveFoamDistortion number? + * @field waveFoamIntensity number? + * @field causticsResolution number? + * @field causticsStrength number? + * @field numTiles integer? + * @field shoreWaves boolean? + * @field forceRendering boolean? + * @field hasWaterPlane boolean? */ /*** @@ -4885,7 +4888,7 @@ int LuaUnsyncedCtrl::PreloadSoundItem(lua_State* L) /*** @function Spring.LoadModelTextures * - * @param modelName string + * @param modelName string? * @return boolean? success */ int LuaUnsyncedCtrl::LoadModelTextures(lua_State* L) @@ -5470,6 +5473,7 @@ int LuaUnsyncedCtrl::Start(lua_State* L) * Note: *.ico images are not supported. * * @param iconFileName string + * @param autoFree boolean? * @return nil */ int LuaUnsyncedCtrl::SetWMIcon(lua_State* L) diff --git a/rts/Lua/LuaUnsyncedRead.cpp b/rts/Lua/LuaUnsyncedRead.cpp index 28f67f80dfc..91919e111ef 100644 --- a/rts/Lua/LuaUnsyncedRead.cpp +++ b/rts/Lua/LuaUnsyncedRead.cpp @@ -94,6 +94,7 @@ #include + /****************************************************************************** * Callouts to get state * @@ -485,7 +486,7 @@ static size_t PushSparseUnitTallyByDef(lua_State *const L, const T &v) * * @function Spring.IsReplay * - * @return boolean? isReplay + * @return boolean isReplay */ int LuaUnsyncedRead::IsReplay(lua_State* L) { @@ -4120,7 +4121,7 @@ int LuaUnsyncedRead::GetCurrentTooltip(lua_State* L) /*** * @function Spring.GetKeyFromScanSymbol - * @param scanSymbol string + * @param scanSymbol string? * @return string keyName */ int LuaUnsyncedRead::GetKeyFromScanSymbol(lua_State* L) @@ -5043,7 +5044,7 @@ int LuaUnsyncedRead::GetGroundDecalTextures(lua_State* L) /*** * - * @function Spring.SetGroundDecalTextureParams + * @function Spring.GetGroundDecalTextureParams * @param decalID integer * @return number? texWrapDistance If non-zero, sets the mode to repeat the texture along the left-right direction of the decal every texWrapFactor elmos. * @return number texTraveledDistance Shifts the texture repetition defined by texWrapFactor so the texture of a next line in the continuous multiline can start where the previous finished. For that it should collect all elmo lengths of the previously set multiline segments. diff --git a/rts/Lua/LuaVBOImpl.cpp b/rts/Lua/LuaVBOImpl.cpp index 9b1cfa00ee7..75cb5265d16 100644 --- a/rts/Lua/LuaVBOImpl.cpp +++ b/rts/Lua/LuaVBOImpl.cpp @@ -673,7 +673,7 @@ size_t LuaVBOImpl::Upload(const sol::stack_table& luaTblData, sol::optional * @param elementCount number? number of elements to download * @param forceGPURead boolean? (Default: `false`) force downloading the data from GPU buffer as opposed * to using shadow RAM buffer - * @return [number, ...][] vboData + * @return number[] vboData */ sol::as_table_t> LuaVBOImpl::Download(sol::optional attribIdxOpt, sol::optional elemOffsetOpt, sol::optional elemCountOpt, sol::optional forceGPUReadOpt) { diff --git a/rts/Sim/Units/Scripts/LuaUnitScript.cpp b/rts/Sim/Units/Scripts/LuaUnitScript.cpp index 1b6f4e7da4a..92047c1206e 100644 --- a/rts/Sim/Units/Scripts/LuaUnitScript.cpp +++ b/rts/Sim/Units/Scripts/LuaUnitScript.cpp @@ -1023,6 +1023,13 @@ void CLuaUnitScript::EndBurst(int weaponNum) { ZoneScoped; Call(LUAFN_EndBurst, /******************************************************************************/ +/*** + * UnitScript API — controls unit animation, piece visibility, and COB values. + * Accessed via `Spring.UnitScript` (synced only). + * + * @see Spring.UnitScript + * @class UnitScriptTable + */ bool CLuaUnitScript::PushEntries(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; @@ -1144,6 +1151,13 @@ static inline int ParseAxis(lua_State* L, const char* caller, int index) /******************************************************************************/ +/*** Create a Lua unit script for the given unit, replacing any existing script. + * + * @function UnitScriptTable.CreateScript + * @param unitID integer + * @param callIns table + * @return nil + */ int CLuaUnitScript::CreateScript(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; @@ -1172,6 +1186,14 @@ int CLuaUnitScript::CreateScript(lua_State* L) } +/*** Update or remove a callIn on a unit script. + * + * @function UnitScriptTable.UpdateCallIn + * @param unitID integer + * @param callin string + * @param func function? + * @return nil + */ int CLuaUnitScript::UpdateCallIn(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; @@ -1196,6 +1218,14 @@ int CLuaUnitScript::UpdateCallIn(lua_State* L) } +/*** Execute a function in the context of a unit's script environment. + * + * @function UnitScriptTable.CallAsUnit + * @param unitID integer + * @param func function + * @param ... any arguments passed to func + * @return any ... values returned by func + */ int CLuaUnitScript::CallAsUnit(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; @@ -1264,6 +1294,14 @@ int CLuaUnitScript::GetUnitValue(lua_State* L, CUnitScript* script, int arg) } +/*** Get a COB/script value for a unit (synced gadget API). + * + * @function UnitScriptTable.GetUnitCOBValue + * @param unitID integer + * @param val integer COB value ID (use COB constants) + * @param ... number optional extra args for certain COB values + * @return integer value + */ int CLuaUnitScript::GetUnitCOBValue(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; @@ -1276,6 +1314,12 @@ int CLuaUnitScript::GetUnitCOBValue(lua_State* L) } +/*** Get a COB/script value. Must be called from within a UnitScript callin. + * + * @function UnitScriptTable.GetUnitValue + * @param val integer COB value ID (use COB constants) + * @return integer value + */ int CLuaUnitScript::GetUnitValue(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; @@ -1307,6 +1351,14 @@ int CLuaUnitScript::SetUnitValue(lua_State* L, CUnitScript* script, int arg) } +/*** Set a COB/script value for a unit (synced gadget API). + * + * @function UnitScriptTable.SetUnitCOBValue + * @param unitID integer + * @param val integer COB value ID (use COB constants) + * @param param integer|boolean value to set + * @return nil + */ int CLuaUnitScript::SetUnitCOBValue(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; @@ -1319,6 +1371,13 @@ int CLuaUnitScript::SetUnitCOBValue(lua_State* L) } +/*** Set a COB/script value. Must be called from within a UnitScript callin. + * + * @function UnitScriptTable.SetUnitValue + * @param val integer COB value ID (use COB constants) + * @param param integer|boolean value to set + * @return nil + */ int CLuaUnitScript::SetUnitValue(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; @@ -1329,6 +1388,13 @@ int CLuaUnitScript::SetUnitValue(lua_State* L) } +/*** Set visibility of a piece. Must be called from within a UnitScript callin. + * + * @function UnitScriptTable.SetPieceVisibility + * @param piece integer 1-indexed piece number + * @param visible boolean + * @return nil + */ int CLuaUnitScript::SetPieceVisibility(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; @@ -1346,6 +1412,13 @@ int CLuaUnitScript::SetPieceVisibility(lua_State* L) } +/*** Emit a special effect at a piece. Must be called from within a UnitScript callin. + * + * @function UnitScriptTable.EmitSfx + * @param piece integer 1-indexed piece number + * @param type integer|string SFX type constant or CEG name + * @return nil + */ int CLuaUnitScript::EmitSfx(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; @@ -1362,6 +1435,13 @@ int CLuaUnitScript::EmitSfx(lua_State* L) } +/*** Attach a unit to a piece of another unit. + * + * @function UnitScriptTable.AttachUnit + * @param piece integer 1-indexed piece number + * @param transporteeID integer + * @return nil + */ int CLuaUnitScript::AttachUnit(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; @@ -1379,6 +1459,12 @@ int CLuaUnitScript::AttachUnit(lua_State* L) } +/*** Drop a transported unit. + * + * @function UnitScriptTable.DropUnit + * @param transporteeID integer + * @return nil + */ int CLuaUnitScript::DropUnit(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; @@ -1396,6 +1482,13 @@ int CLuaUnitScript::DropUnit(lua_State* L) } +/*** Explode a piece with the given flags. + * + * @function UnitScriptTable.Explode + * @param piece integer 1-indexed piece number + * @param flags integer SFX explosion flags (bitfield) + * @return nil + */ int CLuaUnitScript::Explode(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; @@ -1410,6 +1503,12 @@ int CLuaUnitScript::Explode(lua_State* L) } +/*** Show a flare at a piece. + * + * @function UnitScriptTable.ShowFlare + * @param piece integer 1-indexed piece number + * @return nil + */ int CLuaUnitScript::ShowFlare(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; @@ -1423,6 +1522,15 @@ int CLuaUnitScript::ShowFlare(lua_State* L) } +/*** Start spinning a piece around an axis. + * + * @function UnitScriptTable.Spin + * @param piece integer 1-indexed piece number + * @param axis integer axis (1=x, 2=y, 3=z) + * @param speed number angular speed in radians/frame + * @param accel number? angular acceleration (Default: `0`, instant) + * @return nil + */ int CLuaUnitScript::Spin(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; @@ -1440,6 +1548,14 @@ int CLuaUnitScript::Spin(lua_State* L) } +/*** Stop spinning a piece. + * + * @function UnitScriptTable.StopSpin + * @param piece integer 1-indexed piece number + * @param axis integer axis (1=x, 2=y, 3=z) + * @param decel number? angular deceleration (Default: `0`, instant) + * @return nil + */ int CLuaUnitScript::StopSpin(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; @@ -1456,6 +1572,15 @@ int CLuaUnitScript::StopSpin(lua_State* L) } +/*** Turn a piece to an angle. If speed is 0 or omitted, turns instantly. + * + * @function UnitScriptTable.Turn + * @param piece integer 1-indexed piece number + * @param axis integer axis (1=x, 2=y, 3=z) + * @param destination number target angle in radians + * @param speed number? angular speed (Default: `0`, instant) + * @return nil + */ int CLuaUnitScript::Turn(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; @@ -1479,6 +1604,15 @@ int CLuaUnitScript::Turn(lua_State* L) } +/*** Move a piece along an axis. If speed is 0 or omitted, moves instantly. + * + * @function UnitScriptTable.Move + * @param piece integer 1-indexed piece number + * @param axis integer axis (1=x, 2=y, 3=z) + * @param destination number target position + * @param speed number? movement speed (Default: `0`, instant) + * @return nil + */ int CLuaUnitScript::Move(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; @@ -1501,6 +1635,14 @@ int CLuaUnitScript::Move(lua_State* L) return 0; } +/*** Scale a piece. If speed is 0 or omitted, scales instantly. + * + * @function UnitScriptTable.Scale + * @param piece integer 1-indexed piece number + * @param destination number target scale factor + * @param speed number? scaling speed (Default: `0`, instant) + * @return nil + */ int CLuaUnitScript::Scale(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; @@ -1593,6 +1735,13 @@ int CLuaUnitScript::IsInAnimation(lua_State* L, const char* caller, AnimType typ } +/*** Check if a piece is currently turning. + * + * @function UnitScriptTable.IsInTurn + * @param piece integer 1-indexed piece number + * @param axis integer axis (1=x, 2=y, 3=z) + * @return boolean + */ int CLuaUnitScript::IsInTurn(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; @@ -1600,6 +1749,13 @@ int CLuaUnitScript::IsInTurn(lua_State* L) } +/*** Check if a piece is currently moving. + * + * @function UnitScriptTable.IsInMove + * @param piece integer 1-indexed piece number + * @param axis integer axis (1=x, 2=y, 3=z) + * @return boolean + */ int CLuaUnitScript::IsInMove(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; @@ -1607,12 +1763,25 @@ int CLuaUnitScript::IsInMove(lua_State* L) } +/*** Check if a piece is currently spinning. + * + * @function UnitScriptTable.IsInSpin + * @param piece integer 1-indexed piece number + * @param axis integer axis (1=x, 2=y, 3=z) + * @return boolean + */ int CLuaUnitScript::IsInSpin(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; return IsInAnimation(L, __func__, ASpin); } +/*** Check if a piece is currently scaling. + * + * @function UnitScriptTable.IsInScale + * @param piece integer 1-indexed piece number + * @return boolean + */ int CLuaUnitScript::IsInScale(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; @@ -1645,6 +1814,13 @@ int CLuaUnitScript::WaitForAnimation(lua_State* L, const char* caller, AnimType } +/*** Check whether the calling thread needs to wait for a turn to finish. + * + * @function UnitScriptTable.WaitForTurn + * @param piece integer 1-indexed piece number + * @param axis integer axis (1=x, 2=y, 3=z) + * @return boolean needsWait + */ int CLuaUnitScript::WaitForTurn(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; @@ -1652,12 +1828,25 @@ int CLuaUnitScript::WaitForTurn(lua_State* L) } +/*** Check whether the calling thread needs to wait for a move to finish. + * + * @function UnitScriptTable.WaitForMove + * @param piece integer 1-indexed piece number + * @param axis integer axis (1=x, 2=y, 3=z) + * @return boolean needsWait + */ int CLuaUnitScript::WaitForMove(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; return WaitForAnimation(L, __func__, AMove); } +/*** Check whether the calling thread needs to wait for a scale to finish. + * + * @function UnitScriptTable.WaitForScale + * @param piece integer 1-indexed piece number + * @return boolean needsWait + */ int CLuaUnitScript::WaitForScale(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; @@ -1677,6 +1866,12 @@ int CLuaUnitScript::WaitForScale(lua_State* L) } +/*** Signal that the Killed callin has finished. Must be called from Killed. + * + * @function UnitScriptTable.SetDeathScriptFinished + * @param wreckLevel integer? (Default: `-1`) + * @return nil + */ int CLuaUnitScript::SetDeathScriptFinished(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; @@ -1694,6 +1889,14 @@ int CLuaUnitScript::SetDeathScriptFinished(lua_State* L) /******************************************************************************/ +/*** Get the local translation of a piece relative to its rest position. + * + * @function UnitScriptTable.GetPieceTranslation + * @param piece integer 1-indexed piece number + * @return number x + * @return number y + * @return number z + */ int CLuaUnitScript::GetPieceTranslation(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; @@ -1705,6 +1908,14 @@ int CLuaUnitScript::GetPieceTranslation(lua_State* L) } +/*** Get the rotation of a piece in radians. + * + * @function UnitScriptTable.GetPieceRotation + * @param piece integer 1-indexed piece number + * @return number rx + * @return number ry + * @return number rz + */ int CLuaUnitScript::GetPieceRotation(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; @@ -1715,6 +1926,12 @@ int CLuaUnitScript::GetPieceRotation(lua_State* L) return ToLua(L, piece->GetRotation()); } +/*** Get the scale factor of a piece. + * + * @function UnitScriptTable.GetPieceScale + * @param piece integer 1-indexed piece number + * @return number scale + */ int CLuaUnitScript::GetPieceScale(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; @@ -1727,6 +1944,17 @@ int CLuaUnitScript::GetPieceScale(lua_State* L) } +/*** Get the world-space emit position and direction of a piece. + * + * @function UnitScriptTable.GetPiecePosDir + * @param piece integer 1-indexed piece number + * @return number posX + * @return number posY + * @return number posZ + * @return number dirX + * @return number dirY + * @return number dirZ + */ int CLuaUnitScript::GetPiecePosDir(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; @@ -1749,6 +1977,11 @@ int CLuaUnitScript::GetPiecePosDir(lua_State* L) /******************************************************************************/ /******************************************************************************/ +/*** Get the unit ID of the currently executing unit script. + * + * @function UnitScriptTable.GetActiveUnitID + * @return integer? unitID + */ int CLuaUnitScript::GetActiveUnitID(lua_State* L) { RECOIL_DETAILED_TRACY_ZONE; From f2a681d17066c0402beacd4a7e35e8d13925c33e Mon Sep 17 00:00:00 2001 From: Gajo Petrovic Date: Thu, 30 Jul 2026 00:20:26 +0900 Subject: [PATCH 89/90] Tweak ASAN compile & fix a couple of bugs (#2663) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Improve USE_ASAN configuration for all build types - Remove restriction to DEBUG builds only - ASAN works well with optimized builds - Add -fPIC for shared library compatibility with ASAN - Add -fno-omit-frame-pointer for better stack traces in error reports - Add -fno-optimize-sibling-calls to preserve full call chains - Set -O2 optimization level for better error detection than -O3 - Add explanatory comments for each flag 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * Fix stack-use-after-scope in TextureCreationParams Replace std::initializer_list with std::array in wrapModes field. std::initializer_list is just a view into a temporary array, so storing it as a member variable creates a dangling pointer once the temporary is destroyed. ASAN error: ==559461==ERROR: AddressSanitizer: stack-use-after-scope on address 0x74d3039d3460 at pc 0x55c80ddb562f bp 0x7fffc14493c0 sp 0x7fffc14493b0 READ of size 4 at 0x74d3039d3460 thread T0 (recoil-main) #0 0x55c80ddb562e in GL::Impl::InitTexture(GL::TextureCreationParams const&, unsigned int, int) /build/src/rts/Rendering/Textures/Texture.cpp:33 #1 0x55c80ddb721c in GL::Texture2D::Texture2D(int, int, unsigned int, GL::TextureCreationParams const&, bool) /build/src/rts/Rendering/Textures/Texture.cpp:133 #2 0x55c80daf951c in CHeightTexture::CHeightTexture() /build/src/rts/Rendering/Map/InfoTexture/Modern/Height.cpp:49 #3 0x55c80db02149 in CInfoTextureHandler::CInfoTextureHandler() /build/src/rts/Rendering/Map/InfoTexture/Modern/InfoTextureHandler.cpp:26 #4 0x55c80daec745 in std::__detail::_MakeUniq::__single_object std::make_unique() /usr/include/c++/13/bits/unique_ptr.h:1070 #5 0x55c80daec745 in IInfoTextureHandler::Create() /build/src/rts/Rendering/Map/InfoTexture/IInfoTextureHandler.cpp:25 #6 0x55c80df3b1d1 in CWorldDrawer::InitPost() const /build/src/rts/Rendering/WorldDrawer.cpp:111 #7 0x55c8107955f2 in CGame::PostLoadRendering() /build/src/rts/Game/Game.cpp:756 #8 0x55c8107955f2 in CGame::Load(std::__cxx11::basic_string, std::allocator > const&) /build/src/rts/Game/Game.cpp:411 #9 0x55c81084498e in CLoadScreen::Init() /build/src/rts/Game/LoadScreen.cpp:146 #10 0x55c810845a80 in CLoadScreen::CreateInstance(std::__cxx11::basic_string, std::allocator >&&, std::__cxx11::basic_string, std::allocator >&&, ILoadSaveHandler*) /build/src/rts/Game/LoadScreen.cpp:215 #11 0x55c810845a80 in CLoadScreen::CreateDeleteInstance(std::__cxx11::basic_string, std::allocator >&&, std::__cxx11::basic_string, std::allocator >&&, ILoadSaveHandler*) /build/src/rts/Game/LoadScreen.cpp:200 #12 0x55c8108758d6 in CPreGame::UpdateClientNet() /build/src/rts/Game/PreGame.cpp:470 #13 0x55c810876b6f in CPreGame::Update() /build/src/rts/Game/PreGame.cpp:241 #14 0x55c80e883938 in SpringApp::Update() /build/src/rts/System/SpringApp.cpp:886 #15 0x55c80e890ffb in SpringApp::Run() /build/src/rts/System/SpringApp.cpp:927 #16 0x55c80e7fb939 in Run(int, char**) /build/src/rts/System/Main.cpp:51 #17 0x55c80cd63653 in main /build/src/rts/System/Main.cpp:104 #18 0x74d30642a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58 #19 0x74d30642a28a in __libc_start_main_impl ../csu/libc-start.c:360 #20 0x55c80ce28fc9 in _start (/home/gajop/projects/spring-projects/spring-bar/build-linux/install/spring+0x931fc9) (BuildId: 4378a67e1e8529ce2acbdc4e3f13182a8665a60b) Address 0x74d3039d3460 is located in stack of thread T0 (recoil-main) at offset 1120 in frame #0 0x55c80daf83cf in CHeightTexture::CHeightTexture() /build/src/rts/Rendering/Map/InfoTexture/Modern/Height.cpp:23 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * Fix heap-use-after-free in SolLuaEventListener with deferred deletion Implement deferred deletion pattern for SolLuaEventListener to prevent use-after-free when OnDetach is called during ProcessEvent execution. The issue occurred when a Lua event handler called SetInnerRML, which destroyed elements and triggered OnDetach callbacks. OnDetach immediately deleted the listener with "delete this", but ProcessEvent was still on the call stack trying to invoke the Lua callback. The fix defers deletion by: 1. Setting m_detached flag in OnDetach instead of immediate deletion 2. Checking the flag at the start of ProcessEvent (handles queued events) 3. Checking the flag at the end of ProcessEvent (handles mid-execution detachment) This ensures the listener object is only deleted when it's safe to do so, after all its methods have returned. ASAN Error: ==704643==ERROR: AddressSanitizer: heap-use-after-free on address 0x5060002a98d8 at pc 0x618574f3991e bp 0x7ffe2f43de80 sp 0x7ffe2f43de70 READ of size 8 at 0x5060002a98d8 thread T0 (recoil-main) #0 in sol::basic_reference::lua_state() const /build/src/rts/lib/sol2/sol.hpp:10244 #1 in sol::protected_function_result sol::basic_protected_function<>::invoke() /build/src/rts/lib/sol2/sol.hpp:20676 #2 in decltype(auto) sol::basic_protected_function<>::call<>(Rml::Event&, Rml::Element*&, Rml::SolLua::SolLuaDocument*&) /build/src/rts/lib/sol2/sol.hpp:20569 #3 in Rml::SolLua::SolLuaEventListener::ProcessEvent(Rml::Event&) /build/src/rts/Rml/SolLua/plugin/SolLuaEventListener.cpp:144 #4 in Rml::EventDispatcher::DispatchEvent() /build/src/rts/lib/RmlUi/Source/Core/EventDispatcher.cpp:194 #5 in Rml::Element::DispatchEvent() /build/src/rts/lib/RmlUi/Source/Core/Element.cpp:1201 #6 in Rml::Context::ProcessMouseButtonUp(int, int) /build/src/rts/lib/RmlUi/Source/Core/Context.cpp:738 #7-53 [Mouse event handling chain up to main] 0x5060002a98d8 is located 24 bytes inside of 56-byte region freed by thread T0 (recoil-main) here: #0 operator delete(void*, unsigned long) #1 in Rml::SolLua::SolLuaEventListener::~SolLuaEventListener() /build/src/rts/Rml/SolLua/plugin/SolLuaEventListener.h:47 #2 in Rml::SolLua::SolLuaEventListener::OnDetach(Rml::Element*) /build/src/rts/Rml/SolLua/plugin/SolLuaEventListener.cpp:118 #3 in Rml::EventDispatcher::~EventDispatcher() /build/src/rts/lib/RmlUi/Source/Core/EventDispatcher.cpp:65 #4 in Rml::ElementMeta::~ElementMeta() /build/src/rts/lib/RmlUi/Source/Core/Element.cpp:94 #5 in Rml::Element::~Element() /build/src/rts/lib/RmlUi/Source/Core/Element.cpp:151 #6-14 [Element destruction chain] #15 in Rml::Element::SetInnerRML() /build/src/rts/lib/RmlUi/Source/Core/Element.cpp:1100 #16-45 [Lua/Sol2 call stack from event handler calling SetInnerRML] #46 in Rml::SolLua::SolLuaEventListener::ProcessEvent(Rml::Event&) /build/src/rts/Rml/SolLua/plugin/SolLuaEventListener.cpp:144 #47 in Rml::EventDispatcher::DispatchEvent() /build/src/rts/lib/RmlUi/Source/Core/EventDispatcher.cpp:194 #48-53 [Outer event handling that triggered the inner event] previously allocated by thread T0 (recoil-main) here: #0 operator new(unsigned long) #1 in Rml::SolLua::functions::addEventListener() /build/src/rts/Rml/SolLua/bind/Element.cpp:47 #2-53 [Lua binding and event registration chain] 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * Fix heap-use-after-free in RmlUI PopLayer by wrapping Context::Render Root cause: Context::Render was exposed to Lua without proper frame management (BeginFrame/PresentFrame). When called from Lua for render-to-texture workflows, PushLayer/PopLayer would execute outside the frame lifecycle, causing the layer stack to underflow and access freed memory. Solution: Wrap the Lua binding for Context::Render with a lambda that calls BeginFrame before and PresentFrame after rendering, ensuring proper layer stack management for all callers including RTT use cases. ASAN error: ==37278==ERROR: AddressSanitizer: heap-use-after-free on address 0x503000460eec at pc 0x5c05ab21d77a bp 0x7ffcce4f73e0 sp 0x7ffcce4f73d0 READ of size 4 at 0x503000460eec thread T0 (recoil-main) #0 0x5c05ab21d779 in RenderInterface_GL3_Recoil::PopLayer() /build/src/rts/Rml/Backends/RmlUi_Renderer_GL3_Recoil.cpp:1727 #1 0x5c05ad40d908 in Rml::RenderManager::PopLayer() /build/src/rts/lib/RmlUi/Source/Core/RenderManager.cpp:326 #2 0x5c05ad2fac86 in Rml::ElementEffects::RenderEffects(Rml::RenderStage) /build/src/rts/lib/RmlUi/Source/Core/ElementEffects.cpp:336 #3 0x5c05ad2a037a in Rml::Element::Render() /build/src/rts/lib/RmlUi/Source/Core/Element.cpp:255 #4 0x5c05ad2a02eb in Rml::Element::Render() /build/src/rts/lib/RmlUi/Source/Core/Element.cpp:253 #5 0x5c05ad2a02eb in Rml::Element::Render() /build/src/rts/lib/RmlUi/Source/Core/Element.cpp:253 #6 0x5c05ad1e000a in Rml::Context::Render() /build/src/rts/lib/RmlUi/Source/Core/Context.cpp:221 #7 0x5c05ab3a434b in bool sol::member_function_wrapper::call(bool (Rml::Context::*&)(), Rml::Context&) /build/src/rts/lib/sol2/sol.hpp:17338 #8 0x5c05ab3a434b in decltype(auto) sol::member_function_wrapper::caller::operator()(bool (Rml::Context::*&)(), Rml::Context&) const /build/src/rts/lib/sol2/sol.hpp:17344 #9 0x5c05ab3a434b in eval >&, sol::member_function_wrapper::caller, bool (Rml::Context::*&)(), Rml::Context&> /build/src/rts/lib/sol2/sol.hpp:16078 #10 0x5c05ab3a434b in decltype(auto) sol::stack::stack_detail::call::caller, bool (Rml::Context::*&)(), Rml::Context&>(sol::types, sol::types<>, std::integer_sequence, lua_State*, int, sol::member_function_wrapper::caller&&, bool (Rml::Context::*&)(), Rml::Context&) /build/src/rts/lib/sol2/sol.hpp:16131 #11 0x5c05ab3a434b in decltype(auto) sol::stack::call::caller, bool (Rml::Context::*&)(), Rml::Context&>(sol::types, sol::types<>, lua_State*, int, sol::member_function_wrapper::caller&&, bool (Rml::Context::*&)(), Rml::Context&) /build/src/rts/lib/sol2/sol.hpp:16150 #12 0x5c05ab3a434b in int sol::stack::call_into_lua::caller, bool (Rml::Context::*&)(), Rml::Context&>(sol::types, sol::types<>, lua_State*, int, sol::member_function_wrapper::caller&&, bool (Rml::Context::*&)(), Rml::Context&) /build/src/rts/lib/sol2/sol.hpp:16198 #13 0x5c05ab3a434b in int sol::call_detail::lua_call_wrapper::call(lua_State*, bool (Rml::Context::*&)(), Rml::Context&) /build/src/rts/lib/sol2/sol.hpp:18103 #14 0x5c05ab3a434b in int sol::call_detail::lua_call_wrapper::call(lua_State*, bool (Rml::Context::*&)()) /build/src/rts/lib/sol2/sol.hpp:18093 #15 0x5c05ab3a434b in int sol::call_detail::call_wrapped(lua_State*, bool (Rml::Context::*&)()) /build/src/rts/lib/sol2/sol.hpp:18506 #16 0x5c05ab3a434b in int sol::u_detail::binding::call_with_(lua_State*, void*) /build/src/rts/lib/sol2/sol.hpp:23023 #17 0x5c05ab3a434b in int sol::u_detail::binding::call_(lua_State*) /build/src/rts/lib/sol2/sol.hpp:23029 #18 0x5c05aa5925ba in sol::detail::lua_cfunction_trampoline(lua_State*, int (*)(lua_State*)) /build/src/rts/lib/sol2/sol.hpp:8398 #19 0x5c05ab361ccf in int sol::detail::static_trampoline<&(int sol::u_detail::binding::call_(lua_State*))>(lua_State*) /build/src/rts/lib/sol2/sol.hpp:8423 #20 0x5c05ab361ccf in int sol::detail::typed_static_trampoline::call_(lua_State*))>(lua_State*) /build/src/rts/lib/sol2/sol.hpp:8490 #21 0x5c05ab361ccf in int sol::u_detail::binding::call(lua_State*) /build/src/rts/lib/sol2/sol.hpp:23034 #22 0x5c05ac11b57f in luaD_precall(lua_State*, lua_TValue*, int) /build/src/rts/lib/lua/src/ldo.cpp:320 #23 0x5c05ac1499a2 in luaV_execute(lua_State*, int) /build/src/rts/lib/lua/src/lvm.cpp:620 #24 0x5c05ac11c484 in luaD_call(lua_State*, lua_TValue*, int) /build/src/rts/lib/lua/src/ldo.cpp:378 #25 0x5c05ac1056b8 in f_call /build/src/rts/lib/lua/src/lapi.cpp:812 #26 0x5c05ac119bb8 in luaD_rawrunprotected(lua_State*, void (*)(lua_State*, void*), void*) /build/src/rts/lib/lua/src/ldo.cpp:116 #27 0x5c05ac11cdc2 in luaD_pcall(lua_State*, void (*)(lua_State*, void*), void*, long, long) /build/src/rts/lib/lua/src/ldo.cpp:464 #28 0x5c05ac10d695 in lua_pcall(lua_State*, int, int, int) /build/src/rts/lib/lua/src/lapi.cpp:833 #29 0x5c05ac11272f in luaB_pcall /build/src/rts/lib/lua/src/lbaselib.cpp:389 #30 0x5c05ac11b57f in luaD_precall(lua_State*, lua_TValue*, int) /build/src/rts/lib/lua/src/ldo.cpp:320 #31 0x5c05ac1499a2 in luaV_execute(lua_State*, int) /build/src/rts/lib/lua/src/lvm.cpp:620 #32 0x5c05ac11c484 in luaD_call(lua_State*, lua_TValue*, int) /build/src/rts/lib/lua/src/ldo.cpp:378 #33 0x5c05ac1056b8 in f_call /build/src/rts/lib/lua/src/lapi.cpp:812 #34 0x5c05ac119bb8 in luaD_rawrunprotected(lua_State*, void (*)(lua_State*, void*), void*) /build/src/rts/lib/lua/src/ldo.cpp:116 #35 0x5c05ac11cdc2 in luaD_pcall(lua_State*, void (*)(lua_State*, void*), void*, long, long) /build/src/rts/lib/lua/src/ldo.cpp:464 #36 0x5c05ac10d695 in lua_pcall(lua_State*, int, int, int) /build/src/rts/lib/lua/src/lapi.cpp:833 #37 0x5c05aa1e5175 in ScopedLuaCall /build/src/rts/Lua/LuaHandle.cpp:397 #38 0x5c05aa1e5175 in CLuaHandle::RunCallInTraceback(lua_State*, LuaHashString const*, std::__cxx11::basic_string, std::allocator >*, int, int, int, bool) /build/src/rts/Lua/LuaHandle.cpp:483 #39 0x5c05aa1e6908 in CLuaHandle::RunCallInTraceback(lua_State*, LuaHashString const&, int, int, int, bool) /build/src/rts/Lua/LuaHandle.cpp:494 #40 0x5c05aa2102b2 in CLuaHandle::RunCallIn(lua_State*, LuaHashString const&, int, int) /build/src/rts/Lua/LuaHandle.h:425 #41 0x5c05aa2102b2 in CLuaHandle::DrawScreenCommon(LuaHashString const&) /build/src/rts/Lua/LuaHandle.cpp:2863 #42 0x5c05aa2102b2 in CLuaHandle::DrawScreen() /build/src/rts/Lua/LuaHandle.cpp:2881 #43 0x5c05ab9492b0 in CEventHandler::DrawScreen() /build/src/rts/System/EventHandler.cpp:708 #44 0x5c05aa24cdef in CLuaInputReceiver::Draw() /build/src/rts/Lua/LuaInputReceiver.cpp:71 #45 0x5c05ad971694 in CGame::DrawInputReceivers() /build/src/rts/Game/Game.cpp:1574 #46 0x5c05ad98b020 in CGame::Draw() /build/src/rts/Game/Game.cpp:1526 #47 0x5c05aba75cce in SpringApp::Update() /build/src/rts/System/SpringApp.cpp:889 #48 0x5c05aba8323b in SpringApp::Run() /build/src/rts/System/SpringApp.cpp:927 #49 0x5c05ab9edb79 in Run(int, char**) /build/src/rts/System/Main.cpp:51 #50 0x5c05a9f546d3 in main /build/src/rts/System/Main.cpp:104 #51 0x782b5082a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58 #52 0x782b5082a28a in __libc_start_main_impl ../csu/libc-start.c:360 #53 0x5c05aa01a049 in _start (/home/gajop/projects/spring-projects/spring-bar/build-linux/install/spring+0x931049) (BuildId: 0100fe9eb07611ab4faec887c8a97ffbdbc3df06) 0x503000460eec is located 28 bytes inside of 32-byte region [0x503000460ed0,0x503000460ef0) freed by thread T0 (recoil-main) here: #0 0x782b512fc4d8 in free ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:52 #1 0x782b40ef9d00 (/lib/x86_64-linux-gnu/libnvidia-glcore.so.535.274.02+0x14f9d00) (BuildId: 513f593c743a6a4f8ccb0183cb093aa171cef856) previously allocated by thread T0 (recoil-main) here: #0 0x782b512fd9c7 in malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:69 #1 0x782b40ef9851 (/lib/x86_64-linux-gnu/libnvidia-glcore.so.535.274.02+0x14f9851) (BuildId: 513f593c743a6a4f8ccb0183cb093aa171cef856) SUMMARY: AddressSanitizer: heap-use-after-free /build/src/rts/Rml/Backends/RmlUi_Renderer_GL3_Recoil.cpp:1727 in RenderInterface_GL3_Recoil::PopLayer() 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * Fix heap-use-after-free in Element::SetClass via deferred deletion When a Lua event handler calls element.inner_rml = "...", it destroys all child elements. If Lua holds references to those children and tries to use them later in the same event handler (e.g., calling SetClass), it causes a use-after-free. Solution: Wrap SetInnerRML in the Lua binding to manually remove children and store them in a deferred deletion list. Children are kept alive until the event processing completes, preventing use-after-free when Lua accesses them. Original ASAN error: ==41062==ERROR: AddressSanitizer: heap-use-after-free on address 0x506000057c28 READ of size 8 at 0x506000057c28 thread T0 (recoil-main) #0 std::__cxx11::basic_string::size() at /usr/include/c++/13/bits/basic_string.h:1060 #6 Rml::ElementStyle::SetClass() at ElementStyle.cpp:255 #7 Rml::Element::SetClass() at Element.cpp:297 #8 [Sol2/Lua binding call chain] 0x506000057c28 is located 8 bytes inside of 64-byte region freed by thread T0: #7 Rml::ElementStyle::~ElementStyle() at ElementStyle.h:51 #11 Rml::Element::~Element() at Element.cpp:151 #26 std::vector::clear() at stl_vector.h:1603 #41 Rml::Element::~Element() at Element.cpp:148 #49 Rml::Element::SetInnerRML() at Element.cpp:1100 #52 [Sol2/Lua binding call chain - SetInnerRML called from Lua] The element was previously allocated during Element::SetClass(): #9 Rml::ElementStyle::SetClass() at ElementStyle.cpp:262 #10 Rml::Element::SetClass() at Element.cpp:297 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Claude Co-authored-by: TarnishedKnight --- rts/Rml/Backends/RmlUi_Backend.cpp | 14 +++++++++++ rts/Rml/SolLua/bind/Context.cpp | 8 ++++++- rts/Rml/SolLua/bind/Element.cpp | 22 +++++++++++++++++- rts/Rml/SolLua/plugin/SolLuaEventListener.cpp | 23 ++++++++++++++++++- rts/Rml/SolLua/plugin/SolLuaEventListener.h | 2 +- 5 files changed, 65 insertions(+), 4 deletions(-) diff --git a/rts/Rml/Backends/RmlUi_Backend.cpp b/rts/Rml/Backends/RmlUi_Backend.cpp index 70383aa17fb..a46ae11982f 100644 --- a/rts/Rml/Backends/RmlUi_Backend.cpp +++ b/rts/Rml/Backends/RmlUi_Backend.cpp @@ -115,6 +115,9 @@ class BackendState : public Rml::Plugin { RmlGui::SVG::DynamicSVGPlugin* svgPlugin; Rml::UniquePtr> element_lua_texture_instancer; + + // Deferred element deletion: elements removed during event processing are kept alive here + std::vector pending_deletes; }; static Rml::UniquePtr state; @@ -124,6 +127,14 @@ bool RmlInitialized() return state && state->initialized; } +// Deferred element deletion helper - called from Lua bindings +// Elements are kept alive until RmlGui::Update() clears them +void AddPendingDelete(Rml::ElementPtr element) +{ + if (RmlInitialized() && element) + state->pending_deletes.push_back(std::move(element)); +} + bool RmlGui::Initialize() { LOG_L(L_INFO, "[RmlUi::%s] Beginning RmlUi Initialization", __func__); @@ -380,6 +391,9 @@ void RmlGui::Update() } state->contexts_to_remove.clear(); } + + // Clear deferred element deletions - safe point outside event processing + state->pending_deletes.clear(); } void RmlGui::RenderFrame() diff --git a/rts/Rml/SolLua/bind/Context.cpp b/rts/Rml/SolLua/bind/Context.cpp index bd40fd4e535..5cc64899396 100644 --- a/rts/Rml/SolLua/bind/Context.cpp +++ b/rts/Rml/SolLua/bind/Context.cpp @@ -33,6 +33,7 @@ #include "../plugin/SolLuaDataModel.h" #include "../plugin/SolLuaDocument.h" +#include "Rml/Backends/RmlUi_Backend.h" #include "sol2/sol.hpp" #include @@ -427,7 +428,12 @@ void bind_context(sol::table& namespace_table, SolLuaPlugin* slp) * @function RmlUi.Context:Render * @return boolean */ - "Render", &Rml::Context::Render, + "Render", [](Rml::Context& self) { + RmlGui::BeginFrame(); + bool result = self.Render(); + RmlGui::PresentFrame(); + return result; + }, /*** * Closes all documents currently loaded with the context. * @function RmlUi.Context:UnloadAllDocuments diff --git a/rts/Rml/SolLua/bind/Element.cpp b/rts/Rml/SolLua/bind/Element.cpp index 3bf3ecc7fea..405e047fa2a 100644 --- a/rts/Rml/SolLua/bind/Element.cpp +++ b/rts/Rml/SolLua/bind/Element.cpp @@ -36,6 +36,8 @@ #include +// Forward declaration for deferred element deletion +extern void AddPendingDelete(Rml::ElementPtr element); namespace Rml::SolLua { @@ -48,6 +50,24 @@ namespace Rml::SolLua self.AddEventListener(event, e, in_capture_phase); } + void setInnerRMLSafe(Rml::Element& self, const Rml::String& rml) + { + // Manually remove all DOM children and defer their deletion + // This prevents use-after-free when Lua holds references to children + while (self.GetNumChildren()) + { + Rml::Element* child = self.GetChild(0); + // RemoveChild returns an ElementPtr which owns the child + Rml::ElementPtr removed = self.RemoveChild(child); + // Store it for deferred deletion + AddPendingDelete(std::move(removed)); + } + + // Now set the new content + if (!rml.empty()) + self.SetInnerRML(rml); + } + void addEventListener(Rml::Element& self, const Rml::String& event, const Rml::String& code, sol::this_state s) { auto state = sol::state_view{ s }; @@ -497,7 +517,7 @@ namespace Rml::SolLua /*** @field RmlUi.Element.id string ID of this element, in the context of ``. */ "id", sol::property(&Rml::Element::GetId, &Rml::Element::SetId), /*** @field RmlUi.Element.inner_rml string Gets or sets the inner RML (markup) content of the element. */ - "inner_rml", sol::property(sol::resolve(&Rml::Element::GetInnerRML), &Rml::Element::SetInnerRML), + "inner_rml", sol::property(sol::resolve(&Rml::Element::GetInnerRML), &functions::setInnerRMLSafe), /*** @field RmlUi.Element.scroll_left integer Gets or sets the number of pixels that the content of the element is scrolled from the left. */ "scroll_left", sol::property(&Rml::Element::GetScrollLeft, &Rml::Element::SetScrollLeft), /*** @field RmlUi.Element.scroll_top integer Gets or sets the number of pixels that the content of the element is scrolled from the top. */ diff --git a/rts/Rml/SolLua/plugin/SolLuaEventListener.cpp b/rts/Rml/SolLua/plugin/SolLuaEventListener.cpp index d50575e93cc..39aee4ef964 100644 --- a/rts/Rml/SolLua/plugin/SolLuaEventListener.cpp +++ b/rts/Rml/SolLua/plugin/SolLuaEventListener.cpp @@ -115,11 +115,28 @@ namespace Rml::SolLua void SolLuaEventListener::OnDetach(Rml::Element* element) { - delete this; + // Mark as detached but don't delete immediately. + // Deletion will happen when ProcessEvent completes or on next ProcessEvent call. + m_detached = true; + m_element = nullptr; } void SolLuaEventListener::ProcessEvent(Rml::Event& event) { + // If we were detached, delete ourselves now that it's safe + if (m_detached) + { + delete this; + return; + } + + // Check if element is still valid (may have been destroyed during event processing) + if (m_element == nullptr) + return; + + if (m_element->GetContext() == nullptr) + return; + auto document = dynamic_cast(m_element->GetOwnerDocument()); if (document != nullptr && m_func.valid()) { @@ -139,6 +156,10 @@ namespace Rml::SolLua ErrorHandler(m_func.lua_state(), std::move(result)); } } + + // After processing, check if we were detached during the callback + if (m_detached) + delete this; } } // namespace Rml::SolLua diff --git a/rts/Rml/SolLua/plugin/SolLuaEventListener.h b/rts/Rml/SolLua/plugin/SolLuaEventListener.h index 5ed59c81a71..3702b0ebb7d 100644 --- a/rts/Rml/SolLua/plugin/SolLuaEventListener.h +++ b/rts/Rml/SolLua/plugin/SolLuaEventListener.h @@ -35,7 +35,6 @@ #include #include - namespace Rml { class Element; @@ -56,6 +55,7 @@ namespace Rml::SolLua private: sol::protected_function m_func; Rml::Element *m_element; + bool m_detached = false; }; } // namespace Rml::SolLua From 2cd3aacd146f793341369d8a6b040da53a65d629 Mon Sep 17 00:00:00 2001 From: lostsquirrel1 Date: Wed, 29 Jul 2026 16:47:06 +0100 Subject: [PATCH 90/90] update changelog --- doc/site/content/changelogs/_index.markdown | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/doc/site/content/changelogs/_index.markdown b/doc/site/content/changelogs/_index.markdown index 0906e9055e3..d771bc03834 100644 --- a/doc/site/content/changelogs/_index.markdown +++ b/doc/site/content/changelogs/_index.markdown @@ -20,7 +20,9 @@ This is the bleeding-edge changelog since version 2026.06, for **pre-release 202 - Added GetPrevFrameChecksum() to the Lua API - Add multi-platform sync testing (amd64-windows/linux + arm64-linux) [PR 2921](https://github.com/beyond-all-reason/RecoilEngine/pull/2921) - Optimize LuaPushNamedFoo using compile time key hashing [PR 2986](https://github.com/beyond-all-reason/RecoilEngine/pull/2986) -- fix(synctest): workaround widget timing nondeterminism [PR 3124](https://github.com/beyond-all-reason/RecoilEngine/pull/3124) +- Fixed (synctest): workaround widget timing nondeterminism [PR 3124](https://github.com/beyond-all-reason/RecoilEngine/pull/3124) +- Remove refrences to 32-bit support [PR 3033](https://github.com/beyond-all-reason/RecoilEngine/pull/3033) +- Improved setup foir ASAN build [PR 2663](https://github.com/beyond-all-reason/RecoilEngine/pull/2663) ## Documentation - Added 'First Steps with the Engine' guide for game developers @@ -58,6 +60,7 @@ This is the bleeding-edge changelog since version 2026.06, for **pre-release 202 - Added GetPrevFrameChecksum() to the Lua API [PR 2922](https://github.com/beyond-all-reason/RecoilEngine/pull/2922) - Fixes to Spring.SetMapShader [PR 3127](https://github.com/beyond-all-reason/RecoilEngine/pull/3127) - MouseHandler: route XButtons (Mouse4/5) as keybinds instead of mouse ownership [PR 2613](https://github.com/beyond-all-reason/RecoilEngine/pull/2613) +- Fixed Lua EmmyLua type annotations [PR 2888](https://github.com/beyond-all-reason/RecoilEngine/pull/2888) ## Misc - Restored lowercasing in FileSystem::GetExtension @@ -111,3 +114,5 @@ This is the bleeding-edge changelog since version 2026.06, for **pre-release 202 - Dump state handles resource packs - Fix units having the wrong path id after loading a save game. [PR 3120](https://github.com/beyond-all-reason/RecoilEngine/pull/3120) - Avoid UB in float-to-short angle casts (fixes arm64/x86 desync) [PR 3075](https://github.com/beyond-all-reason/RecoilEngine/pull/3075) +- Send gameprogress packet on connection initialization [PR 2872](https://github.com/beyond-all-reason/RecoilEngine/pull/2872) +- Separate and make sonar and RADAR jamming function as they logicall should. [PR 2980](https://github.com/beyond-all-reason/RecoilEngine/pull/2980)