Skip to content

Commit

Permalink
Merge pull request #401 from netux/feat/editor-compiler-for-step
Browse files Browse the repository at this point in the history
Editor: @for loop custom step size
  • Loading branch information
Mitcheljager authored Feb 1, 2024
2 parents 0ef2288 + ccb1e73 commit 11f6162
Show file tree
Hide file tree
Showing 4 changed files with 35 additions and 11 deletions.
4 changes: 2 additions & 2 deletions app/javascript/src/lib/OWLanguageLegacy.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = /(?<!\w)through|from|to|in(?: steps of)?/
const punc = ":;,.(){}[]+-\\*"
const octal = /^\-?0o[0-7][0-7_]*/
const hexadecimal = /^\-?0x[\dA-Fa-f][\dA-Fa-f_]*(?:(?:\.[\dA-Fa-f][\dA-Fa-f_]*)?[Pp]\-?\d[\d_]*)?/
Expand Down Expand Up @@ -58,6 +58,7 @@ function tokenBase(stream, state) {
if (stream.match(hexadecimal)) return "number"
if (stream.match(decimal)) return "number"
if (stream.match(customKeywords)) return "atom"
if (stream.match(customOperators)) return "atom"

if (operators.indexOf(ch) > -1) {
stream.next()
Expand Down Expand Up @@ -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"
}

Expand Down
19 changes: 14 additions & 5 deletions app/javascript/src/lib/OWLanguageLinter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
11 changes: 7 additions & 4 deletions app/javascript/src/utils/compiler/for.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
12 changes: 12 additions & 0 deletions spec/javascript/utils/compiler/for.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit 11f6162

Please sign in to comment.