Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Runtime loaded scripts for user defined code (modding) #394

Open
achimmihca opened this issue Feb 15, 2023 · 2 comments
Open

Runtime loaded scripts for user defined code (modding) #394

achimmihca opened this issue Feb 15, 2023 · 2 comments

Comments

@achimmihca
Copy link
Collaborator

Problem or question to be solved

Runtime loaded scripts would be a powerful mechanism to let users add / change features.

For example, it could be used to

  • connect with custom highscore server (fetch / upload scores)
  • connect with custom song server (fetch / upload songs)
  • add custom actions to the song editor
    • modify notes, optimize stuff, etc.
    • fetch lyrics from Deezer or other services
  • add custom game modifiers
    • for example, spawn items on notes, collect them via singing correct pitch, do something when item has been collected
    • define a random "chance time" somewhere in the song, which makes every note a golden note
    • add "life" that deacreses when singing badly and increases when singing well. It your life falls to 0, you're out.
  • alternative scoring for songs
    • currently, the old UltraStar scoring is used (10000 points max, golden notes give double points, some points are for perfect sentences)
    • instead, people could make it such that
      • a perfect sentence increases a multiplier (2 perfect sentences in a row: x2 points, three perfect sentences in a row: x3 points, etc.)
  • since its user defined scripts, it might also allow users to do copyright sensitive stuff on their own risk (e.g. download video from YouTube)

You get the idea.

The Steam Workshop might make for a good way to share user defined scripts.

Requriements

The following is required for this to work well:

  • we must be able to provide a clear interface for the extensions
    • ideally, we could just declare a C# interface to be implemented
  • users must be able to set up a development environment easily (on Windows, Linux, macOS, not on mobile)
    • tools must be free
    • the interface to be implemented must be clear
    • IDE features like auto-complete should be available
  • error messages must be comprehensible
  • debugging features would be nice (evaluate expressions, break, step, etc.)
    • debugging via log statments will always be possible

iOS and tvOS restrictions

Apple is bitchy about runtime loaded code: https://developer.apple.com/app-store/review/guidelines/

2.4.5 Apps distributed via the Mac App Store have some additional requirements to keep in mind:
(iv) They may not download or install standalone apps, kexts, additional code, or resources to add functionality or significantly change the app from what we see during the review process.

2.5 Software Requirements
2.5.2 Apps should be self-contained in their bundles, and may not read or write data outside the designated container area, nor may they download, install, or execute code which introduces or changes features or functionality of the app, including other apps.

Although rules have been relaxed compared to earlier versions, I think runtime loaded scripts would violate Apple guidelines ("execute code which introduces or changes features").

Thus, I will not bother with this on iOS.

Besides, Unity can also only compile for iOS using IL2CPP, i.e. the C# code is compiled to C++ code, which is then compiled to native machine instructions for iOS.
This may be good for performance and security, but it makes dynamic language features unavailable.

Suggested solution/s

This article covers runtime loaded scripts in the context of a Unity game.

There are three basic approaches to adding scripting to Unity games:

  1. Loading in executable code, native or .NET. This is traditionally difficult because modders don’t have good tools to do this. But – this is changing.
  2. Embedding an existing scripting language like Lua in the game. This is easy for modders, but a lot of work for the developers to support, and performance is considerably slower.
  3. Writing a custom scripting language and building an evaluator in the game. This is probably the most work, since it means creating a new language, parser, evaluator, etc., but it lets developers create a domain-specific language that’s tailored precisely to the needs of the game.

I made a prototype for the first two approaches

  1. compile and run C# code at runtime (as described by the article)
    • see branch anst/cs-runtime-loaded-scripts
  2. interpret JavaScript code at runtime using Jint
    • see branch anst/js-runtime-loaded-scripts

Runtime loaded C#

This works surprisingly well! I successfully tested it on Windows and Android.

Pros

  • easy and fast to implement from our side
    • we can provide a C# interface that users implement.
    • user can access existing code of UltraStar Play
      • this means we don't have to provide an additional API
      • this is a huge advantage from a developer perspective (i.e. my perspective)
  • error messages are understandable (usual C# compiler message with file name, method name, line number, column)
  • injection works for the runtime loaded scripts as usual (e.g. inject settings, managers, UI elements, etc.)
  • can provide an adequate dev environment with auto-complete, IDE checks, etc.
    • in the prototype, I set up a dev environment with the DLLs of UltraStar Play.
      • you can open this in VSCode, install C# dev tools, and viola: editor checks, auto-complete, quick fixes etc.
    • the process of creating a dev environment can be automated (i.e. copy DLLs from the game and add some project config files)

Cons

  • I don't think this will work on iOS because it uses dynamic language features of C#
    • As said above, Apple does not allow runtime loaded code on iOS, so this is not relevant anyway.
  • might introduce security issues
    • The named solution does provide ways to limit access to certain classes. But it may be possible to break out of this using Reflection or other magic.
    • I would just say, people are responsible for the mods they execute, "use at your own risk".

Runtime loaded JavaScript / TypeScript

Why JavaScript (and not Python, Ruby, Lua, etc.)

  • pretty ubiquitous language, lots of active developers
  • has high-quality tools (vs code, linters, package manager, etc.)

So far, the same is true for Python. But

  • TypeScript provides an additional layer that
    • provides a solid and flexibla type system, which I consider crucial
      • for example, we could easily remove the browser API and instead add declarations for UltraStar Play's API
      • I did not find the same level of quality in Python's type hints.
    • can compile high-level language features to simpler ones
      • this allows us to have a simple JS interpreter without losing coding comfort
    • I have experience in TypeScript and know that it could meet the requirements

Finally, Jint is a quality JavaScript interpreter in pure C# that

  • supports up-to-date JS language features
  • can make C# objects and classes available in JS
  • has features that would allow to add debugging support (break points, stepping, evaluate)

My approach

  • JavaScript can register certain extensions to UltraStar Play
  • for development, a TypeScript project can be set up
    • this process could be automated
    • declarations of C# classes could be generated via Reflection
      • this includes the declaration of the interface to be implemented

Pros

  • error messages are understandable
  • can provide an adequate dev environment with auto-complete, IDE checks, etc.
    • in the prototype, I set up a dev environment with TypeScript and generated typing for UltraStar Play's API.
    • the process of creating a dev environment can be automated (i.e. provide config files and type declarations)
  • would probably even work on iOS
    • But as I said, I don't care for iOS. Apple does not allow it anyway.

Cons

  • we have to provide an API for interaction with C# world
    • communication could be done using JSON
  • might introduce security issues
    • runtime loaded scripts may ALWAYS introduce security issues. "Use at your own risk".

Your opinion on the topic

Both approaches can meet the requirements.

But I would use the C# approach.

  • creating an API to use from JavaScript is orders of magnitude more work than just using existing C# code
@achimmihca
Copy link
Collaborator Author

In case iOS scripting would be relevant, there seems to be an interpreter for C#: https://github.com/pjc0247/UniScript

However, performance is probably not the best and I am unsure whether it can access the existing C# code of the game.
But nice to know anyway.

@achimmihca
Copy link
Collaborator Author

achimmihca commented Sep 26, 2023

Runtime loaded C#. I successfully tested it on Windows and Android.

This only works on Android with the Mono scripting backend.
However, to build for ARM64 and Google Play, the IL2CPP scripting backend is required.
But IL2CPP compiles ahead-of-time (AOT) and does not support anything in the System.Reflection.Emit namespace (see IL2CPP restrictions).

Thus, runtime loaded C# using Mono.CSharp.dll as explained in the article above is not possible.
Compilation may still work but at runtime, there will be an error message.


In conclusion, TypeScript / JavaScript with Jint may be the better approach when targeting desktop as well as mobile.
OneJS integrates it very well in Unity. It also uses including React for UI dev. But it is only available as a paid asset on the Unity Asset Store.


BTW: https://github.com/SoapCode/UCompile is a lib that takes the same approach, i.e., compile with Mono.Csharp.Evaluator. But this does not work with IL2CPP (Android / iOS)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant