1212 * <p>Three classes of WC3 BJ calls are transformed:
1313 * <ol>
1414 * <li><b>GetHandleId</b> – replaced 1:1 by {@code __wurst_GetHandleId}, whose Lua
15- * implementation uses a stable table counter instead of the WC3 handle ID
16- * (which can desync in Lua mode) .</li>
15+ * implementation uses a stable table counter for selected opaque runtime handle
16+ * families only. Enum-like handle families keep native semantics in Lua.</li>
1717 * <li><b>Hashtable natives</b> ({@code SaveInteger}, {@code LoadBoolean}, …) and
1818 * <b>context-callback natives</b> ({@code ForForce}, {@code ForGroup}, …) –
1919 * replaced 1:1 by their {@code __wurst_} prefixed equivalents, whose Lua
2020 * implementations are provided by {@link de.peeeq.wurstscript.translation.lua.translation.LuaNatives}.</li>
21- * <li><b>All other BJ calls with at least one handle-typed parameter</b> – wrapped
22- * by a generated IM function that first checks each handle param for {@code null}
23- * and returns the type-appropriate default (0 / 0.0 / false / "" / nil), then
24- * delegates to the original BJ function. This matches Jass behavior, which
25- * silently returns defaults on null-handle calls instead of crashing.</li>
21+ * <li><b>Non-native blizzard.j BJ functions with at least one handle-typed parameter</b>
22+ * – wrapped by a generated IM function that first checks each handle param for
23+ * {@code null} and returns the type-appropriate default (0 / 0.0 / false / "" / nil),
24+ * then delegates to the original BJ function. This matches Jass behavior, which
25+ * silently returns defaults on null-handle calls instead of crashing.
26+ * Common.j natives are intentionally excluded: they either accept {@code nil} natively
27+ * (e.g. {@code TriggerRegisterPlayerUnitEvent} with a null filter) or are already
28+ * covered by categories 1 and 2 above.</li>
2629 * </ol>
2730 *
2831 * <p>IS_NATIVE stubs added for category 1 and 2 are recognised by
@@ -69,6 +72,26 @@ public final class LuaNativeLowering {
6972 "EnumDestructablesInRect" , "GetEnumDestructable"
7073 ));
7174
75+ /** True runtime-object handles that should use Lua-side object identity for GetHandleId. */
76+ private static final Set <String > OPAQUE_RUNTIME_HANDLE_TYPES = new HashSet <>(Arrays .asList (
77+ "unit" , "item" , "destructable" , "effect" , "lightning" , "timer" , "trigger" ,
78+ "triggeraction" , "triggercondition" , "boolexpr" , "force" , "group" , "location" ,
79+ "rect" , "region" , "sound" , "dialog" , "button" , "quest" , "questitem" ,
80+ "leaderboard" , "multiboard" , "multiboarditem" , "trackable" , "texttag" ,
81+ "image" , "ubersplat" , "framehandle" , "fogmodifier" , "hashtable"
82+ ));
83+
84+ /**
85+ * When {@code true}, only opaque runtime-handle families (unit, item, timer, …)
86+ * are shimmed via {@code __wurst_GetHandleId}; enum-like handle families
87+ * (eventid, playerevent, …) keep native {@code GetHandleId} semantics.
88+ *
89+ * When {@code false} (safe default), ALL {@code GetHandleId} calls are shimmed
90+ * unconditionally — this matches the pre-selective-shim behaviour and avoids
91+ * any desync risk while the selective logic is being validated.
92+ */
93+ public static final boolean ENABLE_SELECTIVE_GET_HANDLE_ID_SHIMMING = true ;
94+
7295 private LuaNativeLowering () {}
7396
7497 /**
@@ -100,6 +123,7 @@ public static void transform(ImProg prog) {
100123 // Maps original BJ function → replacement (IS_NATIVE stub or nil-safety wrapper).
101124 // Populated lazily during the traversal.
102125 Map <ImFunction , ImFunction > replacements = new LinkedHashMap <>();
126+ Map <String , ImFunction > specialNativeStubs = new LinkedHashMap <>();
103127 // BJ functions that don't need a replacement (not GetHandleId, not hashtable/callback,
104128 // no handle params). Cached to avoid rechecking the same function at every call site.
105129 Set <ImFunction > noReplacement = new HashSet <>();
@@ -114,7 +138,37 @@ public static void transform(ImProg prog) {
114138 public void visit (ImFunctionCall call ) {
115139 super .visit (call );
116140 ImFunction f = call .getFunc ();
141+ if (ENABLE_SELECTIVE_GET_HANDLE_ID_SHIMMING && isCompatGetHandleIdFunction (f )) {
142+ if (shouldRewriteGetHandleId (call )) {
143+ ImFunction replacement = specialNativeStubs .computeIfAbsent ("__wurst_GetHandleId" ,
144+ name -> createNativeStub (name , f ));
145+ if (!deferredAdditions .contains (replacement )) {
146+ deferredAdditions .add (replacement );
147+ }
148+ call .replaceBy (JassIm .ImFunctionCall (
149+ call .attrTrace (), replacement ,
150+ JassIm .ImTypeArguments (),
151+ call .getArguments ().copy (),
152+ false , CallType .NORMAL ));
153+ }
154+ return ;
155+ }
117156 if (!f .isBj ()) return ;
157+ if ("GetHandleId" .equals (f .getName ())) {
158+ if (ENABLE_SELECTIVE_GET_HANDLE_ID_SHIMMING && shouldRewriteGetHandleId (call )) {
159+ ImFunction replacement = specialNativeStubs .computeIfAbsent ("__wurst_GetHandleId" ,
160+ name -> createNativeStub (name , f ));
161+ if (!deferredAdditions .contains (replacement )) {
162+ deferredAdditions .add (replacement );
163+ }
164+ call .replaceBy (JassIm .ImFunctionCall (
165+ call .attrTrace (), replacement ,
166+ JassIm .ImTypeArguments (),
167+ call .getArguments ().copy (),
168+ false , CallType .NORMAL ));
169+ }
170+ return ;
171+ }
118172 if (noReplacement .contains (f )) return ;
119173
120174 if (!replacements .containsKey (f )) {
@@ -139,13 +193,11 @@ public void visit(ImFunctionCall call) {
139193
140194 private ImFunction computeReplacement (ImFunction bj ) {
141195 String name = bj .getName ();
142- if ("GetHandleId" .equals (name )) {
143- return createNativeStub ("__wurst_GetHandleId" , bj );
144- } else if (HASHTABLE_NATIVE_NAMES .contains (name )) {
196+ if (HASHTABLE_NATIVE_NAMES .contains (name )) {
145197 return createNativeStub ("__wurst_" + name , bj );
146198 } else if (CONTEXT_CALLBACK_NATIVE_NAMES .contains (name )) {
147199 return createNativeStub ("__wurst_" + name , bj );
148- } else if (hasHandleParam (bj )) {
200+ } else if (hasHandleParam (bj ) && ! bj . isNative () ) {
149201 return createNilSafeWrapper (bj );
150202 }
151203 return null ;
@@ -252,6 +304,27 @@ static boolean isHandleType(ImType type) {
252304 return !n .equals ("integer" ) && !n .equals ("real" ) && !n .equals ("boolean" ) && !n .equals ("string" );
253305 }
254306
307+ private static boolean shouldRewriteGetHandleId (ImFunctionCall call ) {
308+ if (call .getArguments ().size () != 1 ) {
309+ return true ;
310+ }
311+ return usesLuaObjectIdentityHandleId (call .getArguments ().get (0 ).attrTyp ());
312+ }
313+
314+ public static boolean usesLuaObjectIdentityHandleId (ImType type ) {
315+ if (!(type instanceof ImSimpleType )) {
316+ return false ;
317+ }
318+ String typeName = ((ImSimpleType ) type ).getTypename ();
319+ return OPAQUE_RUNTIME_HANDLE_TYPES .contains (typeName );
320+ }
321+
322+ private static boolean isCompatGetHandleIdFunction (ImFunction f ) {
323+ return f .getParameters ().size () == 1
324+ && f .getName ().endsWith ("_getHandleId" )
325+ && !f .getName ().endsWith ("_getTCHandleId" );
326+ }
327+
255328 /** Returns an IM expression representing the safe default for the given return type. */
256329 private static ImExpr defaultValueExpr (ImType returnType ) {
257330 if (returnType instanceof ImSimpleType ) {
0 commit comments