From 72069bb263119707f0ab90c6048d3f843dbdde32 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sun, 9 Aug 2026 23:29:04 +0200 Subject: [PATCH 1/5] Recognize client-local native values --- .../optimizer/LocalPlayerContextAnalyzer.java | 74 +++++++++-- .../wurstscript/tests/OptimizerTests.java | 117 ++++++++++++++++++ 2 files changed, 182 insertions(+), 9 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/LocalPlayerContextAnalyzer.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/LocalPlayerContextAnalyzer.java index ddf2d9947..615c28630 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/LocalPlayerContextAnalyzer.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/LocalPlayerContextAnalyzer.java @@ -17,7 +17,8 @@ /** * Conservative, flow-insensitive analysis for values and functions which may - * depend on {@code GetLocalPlayer()}. + * depend on client-local native values such as {@code GetLocalPlayer()} or + * camera state. * * Optimizers use this analysis as a barrier. False positives only cost an * optimization; false negatives could move synchronized work into a @@ -25,6 +26,61 @@ */ public final class LocalPlayerContextAnalyzer { + /** + * Native return values which may differ between clients during the same + * synchronized execution. Event response natives are intentionally absent: + * Warcraft synchronizes mouse, key, and frame events before dispatch. + */ + private static final Set CLIENT_LOCAL_VALUE_SOURCES = Set.of( + // Player identity and values explicitly documented as asynchronous. + "GetLocalPlayer", + "GetLocationZ", + + // Camera state belongs to the local client's camera. + "GetCameraMargin", + "GetCameraBoundMinX", + "GetCameraBoundMinY", + "GetCameraBoundMaxX", + "GetCameraBoundMaxY", + "GetCameraField", + "GetCameraTargetPositionX", + "GetCameraTargetPositionY", + "GetCameraTargetPositionZ", + "GetCameraTargetPositionLoc", + "GetCameraEyePositionX", + "GetCameraEyePositionY", + "GetCameraEyePositionZ", + "GetCameraEyePositionLoc", + + // Localized data may vary with the client's language. + "GetLocalizedString", + "GetLocalizedHotkey", + "GetObjectName", + + // Reforged client-local world and client state. + "BlzGetLocalSpecialEffectX", + "BlzGetLocalSpecialEffectY", + "BlzGetLocalSpecialEffectZ", + "BlzGetLocalUnitZ", + "BlzGetLocalClientWidth", + "BlzGetLocalClientHeight", + "BlzGetMouseFocusUnit", + "BlzGetLocale", + + // Frame state can be changed independently on each client. + "BlzFrameGetName", + "BlzFrameGetText", + "BlzFrameGetTextSizeLimit", + "BlzFrameGetEnable", + "BlzFrameGetAlpha", + "BlzFrameGetValue", + "BlzFrameGetParent", + "BlzFrameGetHeight", + "BlzFrameGetWidth", + "BlzFrameGetChildrenCount", + "BlzFrameGetChild" + ); + private final Set localPlayerDependentVars = Collections.newSetFromMap(new IdentityHashMap<>()); private final Set localPlayerDependentReturns = @@ -75,7 +131,7 @@ public boolean isLocalPlayerDependent(Element element) { } if (element instanceof ImFunctionCall) { ImFunctionCall call = (ImFunctionCall) element; - if (isGetLocalPlayer(call.getFunc()) + if (isClientLocalValueSource(call.getFunc()) || localPlayerDependentReturns.contains(call.getFunc())) { return true; } @@ -98,12 +154,12 @@ public boolean isLocalPlayerDependent(Element element) { public boolean functionUsesLocalPlayer(ImFunction function) { return function != null - && (isGetLocalPlayer(function) || functionsUsingLocalPlayer.contains(function)); + && (isClientLocalValueSource(function) || functionsUsingLocalPlayer.contains(function)); } public boolean functionInliningIsLocalPlayerSensitive(ImFunction function) { return function != null - && (isGetLocalPlayer(function) + && (isClientLocalValueSource(function) || functionsDirectlyUsingLocalPlayer.contains(function) || localPlayerDependentReturns.contains(function)); } @@ -113,7 +169,7 @@ public boolean isLocalPlayerDependent(ImVar variable) { } public boolean isLocalPlayerSource(ImFunction function) { - return isGetLocalPlayer(function); + return isClientLocalValueSource(function); } private void analyze(ImProg prog) { @@ -121,7 +177,7 @@ private void analyze(ImProg prog) { for (ImFunction function : ImHelper.calculateFunctionsOfProg(prog)) { returnFact(function); useFact(function); - if (isGetLocalPlayer(function)) { + if (isClientLocalValueSource(function)) { addLocalPlayerSource(function); } else if (!function.isNative()) { indexElement(function.getBody(), function, entryControlFact(function)); @@ -298,7 +354,7 @@ private void indexFunctionCall(ImFunctionCall call, ImFunction owner, Object con if (!called.isNative()) { addEnclosingControlDependency(controlContext, entryControlFact(called)); } - if (isGetLocalPlayer(called)) { + if (isClientLocalValueSource(called)) { functionsDirectlyUsingLocalPlayer.add(owner); addLocalPlayerSource(called); } @@ -508,9 +564,9 @@ private Fact(FactKind kind, Object subject) { } } - private static boolean isGetLocalPlayer(ImFunction function) { + private static boolean isClientLocalValueSource(ImFunction function) { return function != null && function.isNative() - && "GetLocalPlayer".equals(function.getName()); + && CLIENT_LOCAL_VALUE_SOURCES.contains(function.getName()); } } diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/OptimizerTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/OptimizerTests.java index 33ae93fa1..977897656 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/OptimizerTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/OptimizerTests.java @@ -2158,6 +2158,123 @@ public void functionUsingGetLocalPlayerMustNotBeInlined() throws Exception { "transitive GetLocalPlayer wrappers must remain explicit calls"); } + @Test + public void branchMergerMustNotHoistAcrossLocalCameraCondition() throws Exception { + test().lines( + "package test", + "@extern native GetCameraTargetPositionX() returns real", + "native print(integer i)", + "integer result = 0", + "init", + " real cameraX = GetCameraTargetPositionX()", + " if cameraX > 0.", + " result = 41", + " else", + " result = 41", + " print(result)" + ); + + String optimized = Files.toString( + new File("test-output/OptimizerTests_branchMergerMustNotHoistAcrossLocalCameraCondition_opt.j"), + Charsets.UTF_8); + assertTrue(countOccurrences(optimized, "test_result = 41") >= 2, + "statements must not be hoisted across a client-local camera condition"); + } + + @Test + public void clientLocalNativeValuesAreLocalitySources() { + java.util.Set localValueSources = new java.util.LinkedHashSet<>(java.util.Arrays.asList( + "GetLocalPlayer", + "GetLocationZ", + "GetCameraMargin", + "GetCameraBoundMinX", + "GetCameraBoundMinY", + "GetCameraBoundMaxX", + "GetCameraBoundMaxY", + "GetCameraField", + "GetCameraTargetPositionX", + "GetCameraTargetPositionY", + "GetCameraTargetPositionZ", + "GetCameraTargetPositionLoc", + "GetCameraEyePositionX", + "GetCameraEyePositionY", + "GetCameraEyePositionZ", + "GetCameraEyePositionLoc", + "GetLocalizedString", + "GetLocalizedHotkey", + "GetObjectName", + "BlzGetLocalSpecialEffectX", + "BlzGetLocalSpecialEffectY", + "BlzGetLocalSpecialEffectZ", + "BlzGetLocalUnitZ", + "BlzGetLocalClientWidth", + "BlzGetLocalClientHeight", + "BlzGetMouseFocusUnit", + "BlzGetLocale", + "BlzFrameGetName", + "BlzFrameGetText", + "BlzFrameGetTextSizeLimit", + "BlzFrameGetEnable", + "BlzFrameGetAlpha", + "BlzFrameGetValue", + "BlzFrameGetParent", + "BlzFrameGetHeight", + "BlzFrameGetWidth", + "BlzFrameGetChildrenCount", + "BlzFrameGetChild" + )); + java.util.Set synchronizedEventSources = new java.util.LinkedHashSet<>(java.util.Arrays.asList( + "BlzGetTriggerPlayerMouseX", + "BlzGetTriggerPlayerKey", + "BlzGetTriggerFrameValue" + )); + Element trace = Ast.NoExpr(); + ImFunctions functions = JassIm.ImFunctions(); + java.util.Map functionsByName = new java.util.LinkedHashMap<>(); + for (String name : localValueSources) { + ImFunction nativeFunction = nativeIntFunction(trace, name); + functions.add(nativeFunction); + functionsByName.put(name, nativeFunction); + } + for (String name : synchronizedEventSources) { + ImFunction nativeFunction = nativeIntFunction(trace, name); + functions.add(nativeFunction); + functionsByName.put(name, nativeFunction); + } + ImProg prog = JassIm.ImProg( + trace, + JassIm.ImVars(), + functions, + JassIm.ImMethods(), + JassIm.ImClasses(), + JassIm.ImTypeClassFuncs(), + new java.util.HashMap<>() + ); + LocalPlayerContextAnalyzer analyzer = new LocalPlayerContextAnalyzer(prog); + + for (String name : localValueSources) { + assertTrue(analyzer.isLocalPlayerSource(functionsByName.get(name)), + name + " must be treated as a client-local value source"); + } + for (String name : synchronizedEventSources) { + assertFalse(analyzer.isLocalPlayerSource(functionsByName.get(name)), + name + " belongs to a synchronized event context"); + } + } + + private static ImFunction nativeIntFunction(Element trace, String name) { + return JassIm.ImFunction( + trace, + name, + JassIm.ImTypeVars(), + JassIm.ImVars(), + TypesHelper.imInt(), + JassIm.ImVars(), + JassIm.ImStmts(), + Collections.singletonList(FunctionFlagEnum.IS_NATIVE) + ); + } + @Test(timeOut = 10_000) public void deeplyNestedIndependentCallsDoNotCauseExponentialLocalPlayerAnalysis() { String nestedCall = "Player(0)"; From d39511399ab0b68a9f9a45d04756b97126cd1b12 Mon Sep 17 00:00:00 2001 From: Frotty Date: Sun, 9 Aug 2026 23:56:35 +0200 Subject: [PATCH 2/5] Cover equivalent client-local getters --- .../optimizer/LocalPlayerContextAnalyzer.java | 2 + .../wurstscript/tests/OptimizerTests.java | 40 +++++++++++++++---- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/LocalPlayerContextAnalyzer.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/LocalPlayerContextAnalyzer.java index 615c28630..c02cd27f4 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/LocalPlayerContextAnalyzer.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/LocalPlayerContextAnalyzer.java @@ -62,12 +62,14 @@ public final class LocalPlayerContextAnalyzer { "BlzGetLocalSpecialEffectY", "BlzGetLocalSpecialEffectZ", "BlzGetLocalUnitZ", + "BlzGetUnitZ", "BlzGetLocalClientWidth", "BlzGetLocalClientHeight", "BlzGetMouseFocusUnit", "BlzGetLocale", // Frame state can be changed independently on each client. + "BlzFrameIsVisible", "BlzFrameGetName", "BlzFrameGetText", "BlzFrameGetTextSizeLimit", diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/OptimizerTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/OptimizerTests.java index 977897656..281c57de2 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/OptimizerTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/OptimizerTests.java @@ -2159,26 +2159,50 @@ public void functionUsingGetLocalPlayerMustNotBeInlined() throws Exception { } @Test - public void branchMergerMustNotHoistAcrossLocalCameraCondition() throws Exception { + public void branchMergerMustNotHoistAcrossClientLocalConditions() throws Exception { test().lines( + "type unit extends handle", + "type framehandle extends handle", "package test", "@extern native GetCameraTargetPositionX() returns real", + "@extern native BlzGetUnitZ(unit whichUnit) returns real", + "@extern native BlzFrameIsVisible(framehandle frame) returns boolean", + "native getUnit() returns unit", + "native getFrame() returns framehandle", "native print(integer i)", - "integer result = 0", + "integer cameraResult = 0", + "integer unitResult = 0", + "integer frameResult = 0", "init", " real cameraX = GetCameraTargetPositionX()", " if cameraX > 0.", - " result = 41", + " cameraResult = 41", " else", - " result = 41", - " print(result)" + " cameraResult = 41", + " real unitZ = BlzGetUnitZ(getUnit())", + " if unitZ > 0.", + " unitResult = 43", + " else", + " unitResult = 43", + " boolean frameVisible = BlzFrameIsVisible(getFrame())", + " if frameVisible", + " frameResult = 47", + " else", + " frameResult = 47", + " print(cameraResult)", + " print(unitResult)", + " print(frameResult)" ); String optimized = Files.toString( - new File("test-output/OptimizerTests_branchMergerMustNotHoistAcrossLocalCameraCondition_opt.j"), + new File("test-output/OptimizerTests_branchMergerMustNotHoistAcrossClientLocalConditions_opt.j"), Charsets.UTF_8); - assertTrue(countOccurrences(optimized, "test_result = 41") >= 2, + assertTrue(countOccurrences(optimized, "test_cameraResult = 41") >= 2, "statements must not be hoisted across a client-local camera condition"); + assertTrue(countOccurrences(optimized, "test_unitResult = 43") >= 2, + "statements must not be hoisted across a client-local unit Z condition"); + assertTrue(countOccurrences(optimized, "test_frameResult = 47") >= 2, + "statements must not be hoisted across client-local frame visibility"); } @Test @@ -2207,10 +2231,12 @@ public void clientLocalNativeValuesAreLocalitySources() { "BlzGetLocalSpecialEffectY", "BlzGetLocalSpecialEffectZ", "BlzGetLocalUnitZ", + "BlzGetUnitZ", "BlzGetLocalClientWidth", "BlzGetLocalClientHeight", "BlzGetMouseFocusUnit", "BlzGetLocale", + "BlzFrameIsVisible", "BlzFrameGetName", "BlzFrameGetText", "BlzFrameGetTextSizeLimit", From ab48151cddbf7d5c83b24f1971caf32bd892c021 Mon Sep 17 00:00:00 2001 From: Frotty Date: Mon, 10 Aug 2026 00:11:46 +0200 Subject: [PATCH 3/5] Cover local client activity state --- .../optimizer/LocalPlayerContextAnalyzer.java | 1 + .../tests/wurstscript/tests/OptimizerTests.java | 13 ++++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/LocalPlayerContextAnalyzer.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/LocalPlayerContextAnalyzer.java index c02cd27f4..1fb1433bb 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/LocalPlayerContextAnalyzer.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/LocalPlayerContextAnalyzer.java @@ -65,6 +65,7 @@ public final class LocalPlayerContextAnalyzer { "BlzGetUnitZ", "BlzGetLocalClientWidth", "BlzGetLocalClientHeight", + "BlzIsLocalClientActive", "BlzGetMouseFocusUnit", "BlzGetLocale", diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/OptimizerTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/OptimizerTests.java index 281c57de2..8bcc139a0 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/OptimizerTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/OptimizerTests.java @@ -2167,12 +2167,14 @@ public void branchMergerMustNotHoistAcrossClientLocalConditions() throws Excepti "@extern native GetCameraTargetPositionX() returns real", "@extern native BlzGetUnitZ(unit whichUnit) returns real", "@extern native BlzFrameIsVisible(framehandle frame) returns boolean", + "@extern native BlzIsLocalClientActive() returns boolean", "native getUnit() returns unit", "native getFrame() returns framehandle", "native print(integer i)", "integer cameraResult = 0", "integer unitResult = 0", "integer frameResult = 0", + "integer activeClientResult = 0", "init", " real cameraX = GetCameraTargetPositionX()", " if cameraX > 0.", @@ -2189,9 +2191,15 @@ public void branchMergerMustNotHoistAcrossClientLocalConditions() throws Excepti " frameResult = 47", " else", " frameResult = 47", + " boolean activeClient = BlzIsLocalClientActive()", + " if activeClient", + " activeClientResult = 53", + " else", + " activeClientResult = 53", " print(cameraResult)", " print(unitResult)", - " print(frameResult)" + " print(frameResult)", + " print(activeClientResult)" ); String optimized = Files.toString( @@ -2203,6 +2211,8 @@ public void branchMergerMustNotHoistAcrossClientLocalConditions() throws Excepti "statements must not be hoisted across a client-local unit Z condition"); assertTrue(countOccurrences(optimized, "test_frameResult = 47") >= 2, "statements must not be hoisted across client-local frame visibility"); + assertTrue(countOccurrences(optimized, "test_activeClientResult = 53") >= 2, + "statements must not be hoisted across local-client activity state"); } @Test @@ -2234,6 +2244,7 @@ public void clientLocalNativeValuesAreLocalitySources() { "BlzGetUnitZ", "BlzGetLocalClientWidth", "BlzGetLocalClientHeight", + "BlzIsLocalClientActive", "BlzGetMouseFocusUnit", "BlzGetLocale", "BlzFrameIsVisible", From 49ed91f18519c4e2ed3f66a0edc1c901d6ae0646 Mon Sep 17 00:00:00 2001 From: Frotty Date: Mon, 10 Aug 2026 00:27:23 +0200 Subject: [PATCH 4/5] Cover client-local visual handles --- .../optimizer/LocalPlayerContextAnalyzer.java | 28 ++++++++++- .../wurstscript/tests/OptimizerTests.java | 47 ++++++++++++++++++- 2 files changed, 73 insertions(+), 2 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/LocalPlayerContextAnalyzer.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/LocalPlayerContextAnalyzer.java index 1fb1433bb..34c4372e5 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/LocalPlayerContextAnalyzer.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/LocalPlayerContextAnalyzer.java @@ -57,10 +57,36 @@ public final class LocalPlayerContextAnalyzer { "GetLocalizedHotkey", "GetObjectName", - // Reforged client-local world and client state. + // Special effects can be created and mutated independently per client. "BlzGetLocalSpecialEffectX", "BlzGetLocalSpecialEffectY", "BlzGetLocalSpecialEffectZ", + "BlzGetSpecialEffectScale", + "AddSpecialEffect", + "AddSpecialEffectLoc", + "AddSpecialEffectTarget", + "AddSpellEffect", + "AddSpellEffectLoc", + "AddSpellEffectById", + "AddSpellEffectByIdLoc", + "AddSpellEffectTarget", + "AddSpellEffectTargetById", + "LoadEffectHandle", + + // Visibility checks and local mutations can make lightning state diverge. + "AddLightning", + "AddLightningEx", + "MoveLightning", + "MoveLightningEx", + "DestroyLightning", + "GetLightningColorA", + "GetLightningColorR", + "GetLightningColorG", + "GetLightningColorB", + "SetLightningColor", + "LoadLightningHandle", + + // Reforged client-local world and client state. "BlzGetLocalUnitZ", "BlzGetUnitZ", "BlzGetLocalClientWidth", diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/OptimizerTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/OptimizerTests.java index 8bcc139a0..0ee385823 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/OptimizerTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/OptimizerTests.java @@ -2163,18 +2163,25 @@ public void branchMergerMustNotHoistAcrossClientLocalConditions() throws Excepti test().lines( "type unit extends handle", "type framehandle extends handle", + "type lightning extends handle", + "type effect extends handle", "package test", "@extern native GetCameraTargetPositionX() returns real", "@extern native BlzGetUnitZ(unit whichUnit) returns real", "@extern native BlzFrameIsVisible(framehandle frame) returns boolean", "@extern native BlzIsLocalClientActive() returns boolean", + "@extern native AddLightning(string codeName, boolean checkVisibility, real x1, real y1, real x2, real y2) returns lightning", + "@extern native BlzGetSpecialEffectScale(effect whichEffect) returns real", "native getUnit() returns unit", "native getFrame() returns framehandle", + "native getEffect() returns effect", "native print(integer i)", "integer cameraResult = 0", "integer unitResult = 0", "integer frameResult = 0", "integer activeClientResult = 0", + "integer lightningResult = 0", + "integer effectResult = 0", "init", " real cameraX = GetCameraTargetPositionX()", " if cameraX > 0.", @@ -2196,10 +2203,22 @@ public void branchMergerMustNotHoistAcrossClientLocalConditions() throws Excepti " activeClientResult = 53", " else", " activeClientResult = 53", + " lightning bolt = AddLightning(\"CLPB\", true, 0., 0., 128., 128.)", + " if bolt == null", + " lightningResult = 59", + " else", + " lightningResult = 59", + " real effectScale = BlzGetSpecialEffectScale(getEffect())", + " if effectScale > 0.", + " effectResult = 61", + " else", + " effectResult = 61", " print(cameraResult)", " print(unitResult)", " print(frameResult)", - " print(activeClientResult)" + " print(activeClientResult)", + " print(lightningResult)", + " print(effectResult)" ); String optimized = Files.toString( @@ -2213,6 +2232,10 @@ public void branchMergerMustNotHoistAcrossClientLocalConditions() throws Excepti "statements must not be hoisted across client-local frame visibility"); assertTrue(countOccurrences(optimized, "test_activeClientResult = 53") >= 2, "statements must not be hoisted across local-client activity state"); + assertTrue(countOccurrences(optimized, "test_lightningResult = 59") >= 2, + "statements must not be hoisted across visibility-dependent lightning creation"); + assertTrue(countOccurrences(optimized, "test_effectResult = 61") >= 2, + "statements must not be hoisted across client-local effect state"); } @Test @@ -2240,6 +2263,28 @@ public void clientLocalNativeValuesAreLocalitySources() { "BlzGetLocalSpecialEffectX", "BlzGetLocalSpecialEffectY", "BlzGetLocalSpecialEffectZ", + "AddSpecialEffect", + "AddSpecialEffectLoc", + "AddSpecialEffectTarget", + "AddSpellEffect", + "AddSpellEffectLoc", + "AddSpellEffectById", + "AddSpellEffectByIdLoc", + "AddSpellEffectTarget", + "AddSpellEffectTargetById", + "BlzGetSpecialEffectScale", + "LoadEffectHandle", + "AddLightning", + "AddLightningEx", + "MoveLightning", + "MoveLightningEx", + "DestroyLightning", + "GetLightningColorA", + "GetLightningColorR", + "GetLightningColorG", + "GetLightningColorB", + "SetLightningColor", + "LoadLightningHandle", "BlzGetLocalUnitZ", "BlzGetUnitZ", "BlzGetLocalClientWidth", From baba1e2b05d1a265d728f155d3302f30fb1bdfcc Mon Sep 17 00:00:00 2001 From: Frotty Date: Mon, 10 Aug 2026 00:46:44 +0200 Subject: [PATCH 5/5] Limit locality sources to intrinsic state --- .../optimizer/LocalPlayerContextAnalyzer.java | 50 +--------- .../wurstscript/tests/OptimizerTests.java | 95 ++++--------------- 2 files changed, 20 insertions(+), 125 deletions(-) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/LocalPlayerContextAnalyzer.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/LocalPlayerContextAnalyzer.java index 34c4372e5..33f36ca39 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/LocalPlayerContextAnalyzer.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/intermediatelang/optimizer/LocalPlayerContextAnalyzer.java @@ -28,8 +28,9 @@ public final class LocalPlayerContextAnalyzer { /** * Native return values which may differ between clients during the same - * synchronized execution. Event response natives are intentionally absent: - * Warcraft synchronizes mouse, key, and frame events before dispatch. + * synchronized execution without requiring user code to mutate local state. + * Event responses are synchronized, while handles and UI/audio/visual state + * made local by user code remain the user's responsibility. */ private static final Set CLIENT_LOCAL_VALUE_SOURCES = Set.of( // Player identity and values explicitly documented as asynchronous. @@ -57,35 +58,6 @@ public final class LocalPlayerContextAnalyzer { "GetLocalizedHotkey", "GetObjectName", - // Special effects can be created and mutated independently per client. - "BlzGetLocalSpecialEffectX", - "BlzGetLocalSpecialEffectY", - "BlzGetLocalSpecialEffectZ", - "BlzGetSpecialEffectScale", - "AddSpecialEffect", - "AddSpecialEffectLoc", - "AddSpecialEffectTarget", - "AddSpellEffect", - "AddSpellEffectLoc", - "AddSpellEffectById", - "AddSpellEffectByIdLoc", - "AddSpellEffectTarget", - "AddSpellEffectTargetById", - "LoadEffectHandle", - - // Visibility checks and local mutations can make lightning state diverge. - "AddLightning", - "AddLightningEx", - "MoveLightning", - "MoveLightningEx", - "DestroyLightning", - "GetLightningColorA", - "GetLightningColorR", - "GetLightningColorG", - "GetLightningColorB", - "SetLightningColor", - "LoadLightningHandle", - // Reforged client-local world and client state. "BlzGetLocalUnitZ", "BlzGetUnitZ", @@ -93,21 +65,7 @@ public final class LocalPlayerContextAnalyzer { "BlzGetLocalClientHeight", "BlzIsLocalClientActive", "BlzGetMouseFocusUnit", - "BlzGetLocale", - - // Frame state can be changed independently on each client. - "BlzFrameIsVisible", - "BlzFrameGetName", - "BlzFrameGetText", - "BlzFrameGetTextSizeLimit", - "BlzFrameGetEnable", - "BlzFrameGetAlpha", - "BlzFrameGetValue", - "BlzFrameGetParent", - "BlzFrameGetHeight", - "BlzFrameGetWidth", - "BlzFrameGetChildrenCount", - "BlzFrameGetChild" + "BlzGetLocale" ); private final Set localPlayerDependentVars = diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/OptimizerTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/OptimizerTests.java index 0ee385823..8ace50f65 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/OptimizerTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/OptimizerTests.java @@ -2162,26 +2162,15 @@ public void functionUsingGetLocalPlayerMustNotBeInlined() throws Exception { public void branchMergerMustNotHoistAcrossClientLocalConditions() throws Exception { test().lines( "type unit extends handle", - "type framehandle extends handle", - "type lightning extends handle", - "type effect extends handle", "package test", "@extern native GetCameraTargetPositionX() returns real", "@extern native BlzGetUnitZ(unit whichUnit) returns real", - "@extern native BlzFrameIsVisible(framehandle frame) returns boolean", "@extern native BlzIsLocalClientActive() returns boolean", - "@extern native AddLightning(string codeName, boolean checkVisibility, real x1, real y1, real x2, real y2) returns lightning", - "@extern native BlzGetSpecialEffectScale(effect whichEffect) returns real", "native getUnit() returns unit", - "native getFrame() returns framehandle", - "native getEffect() returns effect", "native print(integer i)", "integer cameraResult = 0", "integer unitResult = 0", - "integer frameResult = 0", "integer activeClientResult = 0", - "integer lightningResult = 0", - "integer effectResult = 0", "init", " real cameraX = GetCameraTargetPositionX()", " if cameraX > 0.", @@ -2193,32 +2182,14 @@ public void branchMergerMustNotHoistAcrossClientLocalConditions() throws Excepti " unitResult = 43", " else", " unitResult = 43", - " boolean frameVisible = BlzFrameIsVisible(getFrame())", - " if frameVisible", - " frameResult = 47", - " else", - " frameResult = 47", " boolean activeClient = BlzIsLocalClientActive()", " if activeClient", " activeClientResult = 53", " else", " activeClientResult = 53", - " lightning bolt = AddLightning(\"CLPB\", true, 0., 0., 128., 128.)", - " if bolt == null", - " lightningResult = 59", - " else", - " lightningResult = 59", - " real effectScale = BlzGetSpecialEffectScale(getEffect())", - " if effectScale > 0.", - " effectResult = 61", - " else", - " effectResult = 61", " print(cameraResult)", " print(unitResult)", - " print(frameResult)", - " print(activeClientResult)", - " print(lightningResult)", - " print(effectResult)" + " print(activeClientResult)" ); String optimized = Files.toString( @@ -2228,14 +2199,8 @@ public void branchMergerMustNotHoistAcrossClientLocalConditions() throws Excepti "statements must not be hoisted across a client-local camera condition"); assertTrue(countOccurrences(optimized, "test_unitResult = 43") >= 2, "statements must not be hoisted across a client-local unit Z condition"); - assertTrue(countOccurrences(optimized, "test_frameResult = 47") >= 2, - "statements must not be hoisted across client-local frame visibility"); assertTrue(countOccurrences(optimized, "test_activeClientResult = 53") >= 2, "statements must not be hoisted across local-client activity state"); - assertTrue(countOccurrences(optimized, "test_lightningResult = 59") >= 2, - "statements must not be hoisted across visibility-dependent lightning creation"); - assertTrue(countOccurrences(optimized, "test_effectResult = 61") >= 2, - "statements must not be hoisted across client-local effect state"); } @Test @@ -2260,55 +2225,27 @@ public void clientLocalNativeValuesAreLocalitySources() { "GetLocalizedString", "GetLocalizedHotkey", "GetObjectName", - "BlzGetLocalSpecialEffectX", - "BlzGetLocalSpecialEffectY", - "BlzGetLocalSpecialEffectZ", - "AddSpecialEffect", - "AddSpecialEffectLoc", - "AddSpecialEffectTarget", - "AddSpellEffect", - "AddSpellEffectLoc", - "AddSpellEffectById", - "AddSpellEffectByIdLoc", - "AddSpellEffectTarget", - "AddSpellEffectTargetById", - "BlzGetSpecialEffectScale", - "LoadEffectHandle", - "AddLightning", - "AddLightningEx", - "MoveLightning", - "MoveLightningEx", - "DestroyLightning", - "GetLightningColorA", - "GetLightningColorR", - "GetLightningColorG", - "GetLightningColorB", - "SetLightningColor", - "LoadLightningHandle", "BlzGetLocalUnitZ", "BlzGetUnitZ", "BlzGetLocalClientWidth", "BlzGetLocalClientHeight", "BlzIsLocalClientActive", "BlzGetMouseFocusUnit", - "BlzGetLocale", - "BlzFrameIsVisible", - "BlzFrameGetName", - "BlzFrameGetText", - "BlzFrameGetTextSizeLimit", - "BlzFrameGetEnable", - "BlzFrameGetAlpha", - "BlzFrameGetValue", - "BlzFrameGetParent", - "BlzFrameGetHeight", - "BlzFrameGetWidth", - "BlzFrameGetChildrenCount", - "BlzFrameGetChild" + "BlzGetLocale" )); - java.util.Set synchronizedEventSources = new java.util.LinkedHashSet<>(java.util.Arrays.asList( + java.util.Set intentionallyExcludedSources = new java.util.LinkedHashSet<>(java.util.Arrays.asList( "BlzGetTriggerPlayerMouseX", "BlzGetTriggerPlayerKey", - "BlzGetTriggerFrameValue" + "BlzGetTriggerFrameValue", + "BlzFrameIsVisible", + "BlzGetLocalSpecialEffectX", + "AddLightning", + "MoveLightning", + "LoadEffectHandle", + "LoadLightningHandle", + "LoadFrameHandle", + "GetSoundIsPlaying", + "BlzIsSelectionEnabled" )); Element trace = Ast.NoExpr(); ImFunctions functions = JassIm.ImFunctions(); @@ -2318,7 +2255,7 @@ public void clientLocalNativeValuesAreLocalitySources() { functions.add(nativeFunction); functionsByName.put(name, nativeFunction); } - for (String name : synchronizedEventSources) { + for (String name : intentionallyExcludedSources) { ImFunction nativeFunction = nativeIntFunction(trace, name); functions.add(nativeFunction); functionsByName.put(name, nativeFunction); @@ -2338,9 +2275,9 @@ public void clientLocalNativeValuesAreLocalitySources() { assertTrue(analyzer.isLocalPlayerSource(functionsByName.get(name)), name + " must be treated as a client-local value source"); } - for (String name : synchronizedEventSources) { + for (String name : intentionallyExcludedSources) { assertFalse(analyzer.isLocalPlayerSource(functionsByName.get(name)), - name + " belongs to a synchronized event context"); + name + " is synchronized event data or user-managed local state"); } }