godotengine / godot-proposals

Godot Improvement Proposals (GIPs)
MIT License
1.08k stars 69 forks source link

Add a method to disallow using all global classes in a particular GDScript #4642

Open fbcosentino opened 2 years ago

fbcosentino commented 2 years ago

Describe the project you are working on

Currently working on a project on space exploration (think "No Man's Sky") where players manage their spaceships very technically, customizing the ship's software by writing real code (e.g. rewriting the autopilot).

Previously published a game where player character was a hacker controlling robots by scripting code.

Also planned a mecha robot building game in a style similar to games like Kerbal Space Program but more inclined to software and circuits, but I gave up on the project due to, among other things, the lack of this specific feature.

Describe the problem or limitation you are having in your project

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).

So far, when working 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 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:

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".

Describe the feature / enhancement and how it helps to overcome the problem or limitation

This proposal (implemented in PR https://github.com/godotengine/godot/pull/61831) adds a feature to the GDScript class allowing to disable globals, per script, via a simple method call. 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. Including globals in a script with globals disabled results in the script not compiling or running.

By disabling globals in a script of interest, the GDScript class can be used as a general purpose scripting parser, compiler and interpreter, running inside a safe box and interacting only with what is desired.

Describe how your proposal will work, with code, pseudo-code, mock-ups, and/or diagrams

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

Assuming a file res://user_script.gd (or a String assigned into 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):
    var player_distance = (player_body.global_transform.origin - my_body.global_transform.origin).length()

If this enhancement will not be used often, can it be worked around with a few lines of script?

No, as this requires changes in the GDScript and GDScriptCompiler classes, which are engine code.

Is there a reason why this should be core and not an add-on in the asset library?

An addon is not possible since this cannot be implemented via scripting.

Calinou commented 2 years ago

Related to https://github.com/godotengine/godot-proposals/issues/389.

PS: You can use gdscript as a syntax highlighting language on GitHub. No need for that #// stuff :slightly_smiling_face:

ashtonmeuser commented 1 year ago

@fbcosentino if you're still interested in this issue, I've created a very basic Godot Wasm addon spawned from this proposal (relevant explanatory comment). In short, it should grant reasonable safety when loading arbitrary user-provided scripts. There are many features lacking but if it solves your problem, I'm happy to receive PRs or issues.