diff --git a/app/javascript/src/lib/OWLanguageLegacy.js b/app/javascript/src/lib/OWLanguageLegacy.js index 3acaeb3ee..c902e9fc8 100644 --- a/app/javascript/src/lib/OWLanguageLegacy.js +++ b/app/javascript/src/lib/OWLanguageLegacy.js @@ -27,7 +27,7 @@ const actions = wordSet(["Abort", "Abort If", "Abort If Condition Is False", "Ab const values = wordSet(["Mixin", "Constant", "For", "Each", "Global", "Ability Charge", "Ability Cooldown", "Ability Icon String", "Ability Resource", "Absolute Value", "All Damage Heroes", "All Dead Players", "All Heroes", "All Living Players", "All Players", "All Players Not On Objective", "All Players On Objective", "All Support Heroes", "All Tank Heroes", "Allowed Heroes", "Altitude Of", "Ammo", "And", "Angle Between Vectors", "Angle Difference", "Append To Array", "Arccosine In Degrees", "Arccosine In Radians", "Arcsine In Degrees", "Arcsine In Radians", "Arctangent In Degrees", "Arctangent In Radians", "Array", "Array Contains", "Array Slice", "Assist Count", "Attacker", "Backward", "Button", "Char In String", "Closest Player To", "Color", "Compare", "Control Mode Scoring Percentage", "Control Mode Scoring Team", "Cosine From Degrees", "Cosine From Radians", "Count Of", "Cross Product", "Current Array Element", "Current Array Index", "Current Game Mode", "Current Map", "Custom Color", "Custom String", "Damage Modification Count", "Damage Over Time Count", "Direction From Angles", "Direction Towards", "Distance Between", "Divide", "Dot Product", "Down", "Empty Array", "Entity Count", "Entity Exists", "Evaluate Once", "Event Ability", "Event Damage", "Event Direction", "Event Healing", "Event Player", "Event Was Critical Hit", "Event Was Environment", "Event Was Health Pack", "Eye Position", "Facing Direction Of", "Farthest Player From", "Filtered Array", "First Of", "Flag Position", "Forward", "Game Mode", "Global", "Global Variable", "Has Spawned", "Has Status", "Heal Over Time Count", "Healee", "Healer", "Healing Modification Count", "Health", "Health of Type", "Hero", "Hero Being Duplicated", "Hero Icon String", "Hero Of", "Horizontal Angle From Direction", "Horizontal Angle Towards", "Horizontal Facing Angle Of", "Horizontal Speed Of", "Host Player", "Icon String", "If-Then-Else", "Index Of Array Value", "Index Of String Char", "Input Binding String", "Is Alive", "Is Assembling Heroes", "Is Between Rounds", "Is Button Held", "Is CTF Mode In Sudden Death", "Is Communicating", "Is Communicating Any", "Is Communicating Any Emote", "Is Communicating Any Spray", "Is Communicating Any Voice line", "Is Control Mode Point Locked", "Is Crouching", "Is Dead", "Is Dummy Bot", "Is Duplicating", "Is Firing Primary", "Is Firing Secondary", "Is Flag At Base", "Is Flag Being Carried", "Is Game In Progress", "Is Hero Being Played", "Is In Air", "Is In Alternate Form", "Is In Line of Sight", "Is In Setup", "Is In Spawn Room", "Is In View Angle", "Is Jumping", "Is Match Complete", "Is Meleeing", "Is Moving", "Is Objective Complete", "Is On Ground", "Is On Objective", "Is On Wall", "Is Portrait On Fire", "Is Reloading", "Is Standing", "Is Team On Defense", "Is Team On Offense", "Is True For All", "Is True For Any", "Is Using Ability 1", "Is Using Ability 2", "Is Using Ultimate", "Is Waiting For Players", "Last Assist ID", "Last Created Entity", "Last Created Health Pool", "Last Damage Modification ID", "Last Damage Over Time ID", "Last Heal Over Time ID", "Last Healing Modification ID", "Last Of", "Last Text ID", "Left", "Local Player", "Local Vector Of", "Magnitude Of", "Map", "Match Round", "Match Time", "Max", "Max Ammo", "Max Health", "Max Health of Type", "Min", "Modulo", "Multiply", "Nearest Walkable Position", "Normalize", "Normalized Health", "Not", "Null", "Number of Dead Players", "Number of Deaths", "Number of Eliminations", "Number of Final Blows", "Number of Heroes", "Number of Living Players", "Number of Players", "Number of Players On Objective", "Number of Slots", "Objective Index", "Objective Position", "Opposite Team Of", "Or", "Payload Position", "Payload Progress Percentage", "Player Carrying Flag", "Player Closest To Reticle", "Player Hero Stat", "Player Stat", "Player Variable", "Players In Slot", "Players On Hero", "Players Within Radius", "Players in View Angle", "Point Capture Percentage", "Position Of", "Raise To Power", "Random Integer", "Random Real", "Random Value In Array", "Randomized Array", "Ray Cast Hit Normal", "Ray Cast Hit Player", "Ray Cast Hit Position", "Remove From Array", "Right", "Round To Integer", "Score Of", "Server Load", "Server Load Average", "Server Load Peak", "Sine From Degrees", "Sine From Radians", "Slot Of", "Sorted Array", "Mapped Array", "Spawn Points", "Speed Of", "Speed Of In Direction", "Square Root", "String", "String Contains", "String Length", "String Replace", "String Slice", "String Split", "Subtract", "Tangent From Degrees", "Tangent From Radians", "Team Of", "Team Score", "Text Count", "Throttle Of", "Total Time Elapsed", "Ultimate Charge Percent", "Up", "Update Every Frame", "Value In Array", "Vector", "Vector Towards", "Velocity Of", "Vertical Angle From Direction", "Vertical Angle Towards", "Vertical Facing Angle Of", "Vertical Speed Of", "Victim", "Weapon", "Workshop Setting Combo", "Workshop Setting Hero", "Workshop Setting Integer", "Workshop Setting Real", "Workshop Setting Toggle", "World Vector Of", "X Component Of", "Y Component Of", "Z Component Of", "Damage", "Heal"]) const bools = wordSet(["True", "False"]) const operators = "+-/*%=|&<>~^?!" -const customOperators = wordSet(["through", "from", "to", "in"]) +const customOperators = /(? -1) { stream.next() @@ -95,7 +96,6 @@ function tokenBase(stream, state) { if (stream.match(identifier)) { const ident = stream.current() if (keywords.hasOwnProperty(ident)) return "keyword" - if (customOperators.hasOwnProperty(ident)) return "atom" return "variable" } diff --git a/app/javascript/src/lib/OWLanguageLinter.js b/app/javascript/src/lib/OWLanguageLinter.js index 09cbbc6c5..c227100ab 100644 --- a/app/javascript/src/lib/OWLanguageLinter.js +++ b/app/javascript/src/lib/OWLanguageLinter.js @@ -273,12 +273,21 @@ function checkForLoops(content) { try { const [_, params] = match - if (params[0] != "(") throw new Error("Missing opening parenthesis") - if (params[params.length - 1] != ")") throw new Error("Missing closing parenthesis") - if (!/to|through/.test(params)) throw new Error("Either \"to\" or \"through\" are expected") + if (params[0] !== "(") throw new Error("Missing opening parenthesis") + if (params[params.length - 1] !== ")") throw new Error("Missing closing parenthesis") + + const splitParams = params.split(/\s+/) + const toThroughIndex = splitParams.findIndex((s) => /to|through/.test(s)) + if (toThroughIndex < 0) throw new Error("Either \"to\" or \"through\" are expected") + + if ( + splitParams.length > 3 && + toThroughIndex !== 1 && + splitParams[toThroughIndex - 2] !== "from" + ) { + throw new Error("Missing \"from\" after iterator name") + } - const splitParams = params.split(" ") - if (splitParams.length > 3 && splitParams[1] != "from") throw new Error("Missing \"from\" after iterator name") } catch (error) { diagnostics.push({ from: match.index, diff --git a/app/javascript/src/utils/compiler/for.js b/app/javascript/src/utils/compiler/for.js index 09c6d1a45..95e52b426 100644 --- a/app/javascript/src/utils/compiler/for.js +++ b/app/javascript/src/utils/compiler/for.js @@ -2,19 +2,22 @@ import { getClosingBracket, replaceBetween } from "../parse" export function evaluateForLoops(joinedItems) { let match - const forRegex = /@for\s+\(\s*((?:(\w+)\s+)?(?:from\s+))?(\d+)\s+(?:(through|to)\s+)?(\d+)\s*\)\s*\{/g // Matches "@for ([var] [from] number through|to number) {" in groups for each param + const forRegex = /@for\s+\(\s*((?:(\w+)\s+)?(?:from\s+))?(\d+)\s+(?:(through|to)\s+)?(\d+)(?:\s*in steps of\s+(\d+))?\s*\)\s*\{/g // Matches "@for ([var] [from] number through|to number [in steps of number]) {" in groups for each param while ((match = forRegex.exec(joinedItems)) != null) { - const [full, _, variable, start, clusivity, end] = match + const [full, _, variable, startString, clusivityKeyword, endString, stepString = "1"] = match - const inclusive = clusivity === "through" + const inclusive = clusivityKeyword === "through" const openingBracketIndex = match.index + full.length - 1 const closingBracketIndex = getClosingBracket(joinedItems, "{", "}", openingBracketIndex - 1) const content = joinedItems.substring(openingBracketIndex + 1, closingBracketIndex) + const [start, end, step] = [parseInt(startString), parseInt(endString), parseInt(stepString)] + if (step === 0) throw new Error("For loop would cause an infinite loop") + // Replace "For.[variable]" with the current index let repeatedContent = "" - for(let i = parseInt(start); i < parseInt(end) + (inclusive ? 1 : 0); i++) { + for(let i = start; i < end + (inclusive ? step : 0); i += step) { repeatedContent += content.replaceAll(`For.${ variable || "i" }`, i) } diff --git a/spec/javascript/utils/compiler/for.test.js b/spec/javascript/utils/compiler/for.test.js index 3debfc93b..a8a8ec527 100644 --- a/spec/javascript/utils/compiler/for.test.js +++ b/spec/javascript/utils/compiler/for.test.js @@ -48,6 +48,18 @@ describe("for.js", () => { expect(disregardWhitespace(evaluateForLoops(input))).toBe(disregardWhitespace(expectedOutput)) }) + test("Should evaluate a for loop with a custom step size", () => { + const input = `@for (2 through 6 in steps of 2) { + For.i; + }` + const expectedOutput = ` + 2; + 4; + 6; + ` + expect(disregardWhitespace(evaluateForLoops(input))).toBe(disregardWhitespace(expectedOutput)) + }) + test("Should handle nested for loops", () => { const input = `@for (1 through 2) { Test;