godot icon indicating copy to clipboard operation
godot copied to clipboard

[3.x] Added globals disabled feature to GDScript class

Open fbcosentino opened this issue 2 years ago • 7 comments

Some games and apps involve players typing their own code, which is then interpreted/executed as part of the process.

One example is games where the player is expected to write logic as part of the game mechanic (control robots, hack into fictional systems, etc). There is an entire game genre based on this (https://en.wikipedia.org/wiki/Programming_game).

Another example is games which can be customized by players (mods), including writing script code (e.g. enemy AI).

In the projects I have worked so far with this mechanic, I had to design a scripting language and write my own interpreter (I have seen others trying the same as well). Due to having to write the interpreter, the scripting was rudimentary (sequential) and my players complained they wanted usual programming features like branching, loops, functions, local variables, etc, and were frustrated by the lack of.

GDScript could be used to implement in-game programming with a very functional, complex, stable and user friendly language, and it comes with a compiler for free, zero dev effort. However, due to the access to the engine globals, this comes with severe issues. Players could:

  • Break the game behaviour
  • Call OS and damage something in their computer
  • Call ResourceSaver and steal assets
  • In multiplayer games (or with leaderboards), print to console server confidential credentials

And if players are meant to share scripts (e.g. mods), hell is unleashed with anything from "delete all hdd" pranks to trojan horse attacks.

Therefore, so far using GDScript to power in-game general purpose scripting is "a nice thing you can't have".

This PR adds a feature to the GDScript class allowing to disable globals, per script. All game scripts behave as usual by default, but an individual script can have a flag set so that specific script won't have access to any engine globals, including class names, singletons and constants, becoming detached from the game system. It still can access anything if access is explicitly given, via variables or method arguments.

The code below demonstrates how to use the new set_globals_disabled method.

Assuming a file res://user_script.gd (or a String assigned via GDScript.source_code), which is meant to be isolated:

var my_var

func my_func(arg):
    print("Hello %s!" % arg)

A node can safely run this script with globals disabled:

var new_script = load("res://user_script.gd")
new_script.set_globals_disabled(true)
if new_script.reload():
    # Script recompiled successfully
    var new_script_instance = new_script.new()
    # Any method can be called externally,
    # and variables can be externally assigned
    new_script_instance.my_var = 42
    new_script_instance.my_func("world")  # prints "Hello world!"
else:
    # Pauses with an error if running from the editor,
    # fails silently and continues running in standalone build
    print("Failed to compile script")

An example to give explicit access to something, assuming the res://test_script.gd script:

var input

func do_something():
    if input.is_action_pressed("ui_accept"):
        print("Hello world!")

The input local variable can be used to give the script access to the Input singleton:

var new_script = load("res://test_script.gd")
new_script.set_globals_disabled(true)
if new_script.reload():
    var new_script_instance = new_script.new()
    new_script_instance.set("input", Input) # Fails silently if user doesn't want to declare or use `var input`
    new_script_instance.do_something()

Function arguments also work with external objects. The following script would work even with globals disabled, if ai_search_player is called externally providing the KinematicBody arguments:

func ai_search_player(my_kinematic_body, player_kinematic_body):
    player_distance = (player_body.global_transform.origin - my_body.global_transform.origin).length()

For mod systems where a subset of the engine functionality should be exposed, access to an interface node can be given to the isolated script (like the input example), that node having a selection of methods an properties as required in the modding system. In other words, that node would work like a master singleton for the isolated script.

fbcosentino avatar Jun 08 '22 22:06 fbcosentino