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>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
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 required handle param for
2323 * {@code null} and returns the type-appropriate default (0 / 0.0 / false / "" / nil),
2424 * then delegates to the original BJ function. This matches Jass behavior, which
2525 * 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>
26+ * {@code boolexpr} and {@code code} typed params are intentionally skipped: these
27+ * are optional/nullable in Jass (e.g. the filter arg of
28+ * {@code TriggerRegisterPlayerUnitEvent}) and passing {@code nil} is valid .</li>
2929 * </ol>
3030 *
3131 * <p>IS_NATIVE stubs added for category 1 and 2 are recognised by
@@ -197,7 +197,7 @@ private ImFunction computeReplacement(ImFunction bj) {
197197 return createNativeStub ("__wurst_" + name , bj );
198198 } else if (CONTEXT_CALLBACK_NATIVE_NAMES .contains (name )) {
199199 return createNativeStub ("__wurst_" + name , bj );
200- } else if (hasHandleParam (bj ) && ! bj . isNative () ) {
200+ } else if (hasHandleParam (bj )) {
201201 return createNilSafeWrapper (bj );
202202 }
203203 return null ;
@@ -247,10 +247,12 @@ private static ImFunction createNilSafeWrapper(ImFunction bjNative) {
247247
248248 ImStmts body = JassIm .ImStmts ();
249249
250- // Null-check each handle param: if param == null then return <default> end
250+ // Null-check each required handle param: if param == null then return <default> end
251+ // boolexpr and code params are intentionally skipped — they are optional/nullable
252+ // in Jass (e.g. the filter arg of TriggerRegisterPlayerUnitEvent).
251253 ImExpr returnDefault = defaultValueExpr (bjNative .getReturnType ());
252254 for (ImVar param : paramVars ) {
253- if (isHandleType (param .getType ())) {
255+ if (isHandleType (param .getType ()) && ! isNullableHandleType ( param . getType ()) ) {
254256 ImExpr condition = JassIm .ImOperatorCall (WurstOperator .EQ , JassIm .ImExprs (
255257 JassIm .ImVarAccess (param ),
256258 JassIm .ImNull (param .getType ().copy ())
@@ -304,6 +306,22 @@ static boolean isHandleType(ImType type) {
304306 return !n .equals ("integer" ) && !n .equals ("real" ) && !n .equals ("boolean" ) && !n .equals ("string" );
305307 }
306308
309+ /**
310+ * Returns true for handle types that are valid to pass as {@code null} in Jass without
311+ * triggering a null-handle crash. These params are skipped in nil-safety wrappers.
312+ *
313+ * <p>{@code boolexpr} and {@code code} are the canonical optional types: every WC3
314+ * API that takes them (filter, condition, action callbacks) accepts {@code null} to
315+ * mean "no callback".
316+ */
317+ static boolean isNullableHandleType (ImType type ) {
318+ if (!(type instanceof ImSimpleType )) {
319+ return false ;
320+ }
321+ String n = ((ImSimpleType ) type ).getTypename ();
322+ return n .equals ("boolexpr" ) || n .equals ("code" );
323+ }
324+
307325 private static boolean shouldRewriteGetHandleId (ImFunctionCall call ) {
308326 if (call .getArguments ().size () != 1 ) {
309327 return true ;
@@ -320,9 +338,14 @@ public static boolean usesLuaObjectIdentityHandleId(ImType type) {
320338 }
321339
322340 private static boolean isCompatGetHandleIdFunction (ImFunction f ) {
323- return f .getParameters ().size () == 1
324- && f .getName ().endsWith ("_getHandleId" )
325- && !f .getName ().endsWith ("_getTCHandleId" );
341+ if (f .getParameters ().size () != 1
342+ || !f .getName ().endsWith ("_getHandleId" )
343+ || f .getName ().endsWith ("_getTCHandleId" )) {
344+ return false ;
345+ }
346+ // Restrict to WC3 simple handle types (ImSimpleType). User-defined Wurst classes
347+ // use ImClassType and must not have their call sites replaced.
348+ return isHandleType (f .getParameters ().get (0 ).getType ());
326349 }
327350
328351 /** Returns an IM expression representing the safe default for the given return type. */
0 commit comments