Released Catspeak - MOD YOUR GAMES!

Nux

Member

@Katsaii presents:
Catspeak
A Domain-specific Language for Adding Mod Support to GameMaker Studio 2 Games

Homepage | Wiki | Installation

License
View the full license here. If you use this tool, please make sure to credit me as "Katsaii" under Additional Programming. Feel free to link to my homepage or Twitter.


Sorry, what was is it again?
Catspeak is an entire programming language, compiler, and virtual machine designed for, and written in, GameMaker Studio 2.3!

If you find any bugs, or want to suggest a feature, please open an issue on the GitHub page: https://github.com/NuxiiGit/catspeak-lang/issues.


Features
  • Support for strings ("hello world") and numerical values (12.5)
  • Support for array ([1, 2, 3]) and object ({ .a : "this is a", .b : "this is b" }) literals
  • Groupings of expressions using parenthesis, or the use of the unary grouping operator :
  • Variable assignment using the = operator
  • Control-flow statements such as if, while, for, break, and continue
  • Return values using the return keyword
  • Call GameMaker functions using the run keyword or by passing parameters in a Lisp-style f arg1 arg2 arg3 ...
  • Access elements of collections using ordered (a.[i]) and unordered (a.{"key"} or a.key) index operators
  • Create new functions using the fun keyword and export them to GML using extern
  • Entirely sandboxed runtime environment, only expose functions to modders that you want to expose
  • Execute large and complex programs asynchronously over multiple steps


Why did you make this?
I wanted to.


Why should I use this?
Catspeak is able to load large JSON files over multiple frames. Typically the built-in json_parse function will freeze the game until the entire contents of the file are parsed, CatSpeak instead parses, compiles, and evaluates programs over multiple steps, avoiding the frustrating game freezing.


Is that the only reason?
No. Catspeak is a Turing-complete, imperative programming language that can be customised with additional operators and functions. For instance, a set of functions could be exposed to modders that they can call from within the Catspeak runtime environment. These functions may add additional items to the game, or set the position of existing objects in the room.


Is it safe to use?
Yes! Script IDs and Instance IDs cannot be accessed by Catspeak programs unless a developer wills it. So, trying to access a global variable using (-4).sneakyGlobal is not possible unless the developers of the game enable that feature. All exposed scripts are bound as methods, so it is impossible to call an arbitrary script from within Catspeak without the developer intentionally (or unintentionally) exposing those tools to modders.


Examples
A sweet couple of example programs written in Catspeak:
Code:
-- arrays and loops
planets = [
  "Venus"
  "Earth"
  "Mars"
]
n = 3
i = 0
while (i < n) {
  planet = planets.[i]
  print planet -- Venus
  i = i + 1    -- Earth
}              -- Mars
Code:
-- structs
position = {
  .x : 12
  .y : -3
}
return : -position.x * position.{"y"} -- 36
 
Last edited:

Nux

Member
Introducing For-loops

In the interest of improving the usability of the language, for-loops are now part of Catspeak. These are not your ordinary for-loops in GameMaker, though; these are so-called "foreach" loops. This lets you easily iterate over arrays and other data structures.


Syntax
The general structure to loop through the values of an array is:
Code:
for arr.[idx] = val {
  -- do some stuff with `idx` and `val`
}
The arr variable is the data structure you want to iterate over, either an array or a ds_list. The idx and val variables then store the current index and the value at that index. Both the break and continue keywords also work with for-loops, so go nuts.


Other Syntax
To iterate over an unordered collection (structs, ds_maps, object instances) curly braces {} should be used instead of square parenthesis []. For example, the following code iterates over the keys of a struct and prints them to the console:
Code:
people = { .you : "not cool", .kat : "very cool" }
for people.{key} = val {
  print (key ++ " = " ++ val) -- you = not cool
                              -- kat = very cool
}
If you don't care about one of the arguments of the for-loop, you can ignore it by using the special _ identifier:
Code:
for [1, 2, 3].[_] = number {
  print number -- 1
               -- 2
               -- 3
}

Challenge
Can you figure out what this funky code for a unit test returns?
Code:
result = undefined
for [1, 2].[_] = outer {
  for [3, 4].[_] = inner {
    if (outer == 1) {
      continue 2
    }
    result = inner * outer
    break 2
  }
}
return result
 

Nux

Member
Introducing Functions

Functions are now part of the language! Create new behaviour at runtime and even call certain functions from within GML seamlessly.


Syntax
The general structure of a function is:
Code:
say_hello = fun {
  print "Hello!"
}
Where the fun keyword signifies the start of a function. To access arguments passed to a function, the arg magic variable should be used:
Code:
factorial = fun {
  n = arg.[0]
  if (n <= 1) {
    return 1
  }
  return : n * factorial (n - 1)
}
print : factorial 10 -- 3628800
Named arguments are probably never going to be supported due to their subtle complexity and just general restrictiveness compared to having access to the argument array.


Calling Catspeak Functions From GML
To call a Catspeak function from GML, it must be exported using the extern keyword:
Code:
add_numbers = extern fun {
  return : arg.[0] + arg.[1]
}
This makes sure that the function executes completely within a single step. This is important because then Catspeak functions can be executed exactly like GML functions, i.e. add_numbers(5, 8) in GML.


Variadic Functions
Like I said before, the arg keyword can be used to iterate over all arguments passed to a function, such as the following function that computes the mean of a bunch of numbers:
Code:
mean = fun {
  count = 0
  for arg.[_] = n {
    count = count + n
  }
  return : count / get_length arg
}
print : mean 10 15 30 -- 18.3
 
Last edited:

Nux

Member
Named arguments are probably never going to be supported due to their subtle complexity and just general restrictiveness compared to having access to the argument array.
Okay, I guess I lied...

Introducing Named Function Arguments

You can now define function arguments without touching the arg directly! Simply include the names of the arguments after the fun keyword and you're ready to go. Download the patch here: https://github.com/NuxiiGit/catspeak-lang/releases/tag/1.2.1.


Improving the Factorial Function
The previous factorial function can be updated to use named function arguments:
Code:
factorial = fun n {
  if (n <= 1) {
    return 1
  }
  return : n * factorial (n - 1)
}
print : factorial 10 -- 3628800
This should hopefully improve the readability of functions with many arguments. No default arguments quite yet, but it is on my mind, I just haven't figured out a decent syntax for it.

Please tell me what you think!
 

Nux

Member
A small little patch to change how exporting Catspeak functions to GML works: https://github.com/NuxiiGit/catspeak-lang/releases/tag/1.2.2.

You now need to use the extern keyword:
Code:
callable_by_gml = extern fun message count {
  while (count > 0) {
    count = count - 1
    print message
  }
}
This function would then be called in GML using the typical method: callable_by_gml("hello", 5).

I've also updated the wiki to include a page detailing the standard library of Catspeak, including the operators it offers. If you don't fancy those, you can always define new operators in Catpseak by using identifier literals:
Code:
`|>` = fun x f {
  -- a new fancy operator that applies a value to a function
  return : f x
}

-- using my new operator
print : { .x 1, .y 2 } |> string -- converts the struct into a string and then prints it out
 
Top