VoidSharp - A GMod C# Abstraction Layer

VoidSharp - A Garry’s Mod C# Abstraction Layer

A few months ago I started working on an C# abstraction layer with @Menschlich.
It is a layer on top of GLua, allowing you to use proper OOP C# code which is then transpiled to Lua with CSharp.lua. It’s basically an environment enabling you to write addons in Garry’s Mod with C#. Some code is kinda inspired by S&box.

I managed to get it in a working state, so it’s useable. It is FAR from finished, most of the stuff is missing.
It IS NOT easy to setup, you WILL encounter a lot of issues. You can definitely make some stuff with it, it works (sometimes). Since S&box is around the corner, I have decided to stop working on it and open-source it. The code behind VoidSharp is not that great, but the working-side is okay. It’s just something I worked on before S&box was in a working state and before when I had time.

VoidSharp manages Hooks, Console Commands, Net Messages, UI, Database Connections (with an ORM), and wraps around some gmod libraries - Players, entities, vectors, etc…

The source code of VoidSharp and how to get it working is on GitHub:

And you can view example code using VoidSharp here:

Examples

Let’s start with an example: We want to make a UI in C#. We use the VoidSharp library which exposes a lot of Garry’s Mod functions. The code can look like this:

public class MainPanel : Frame
{
    public MainPanel()
    {
        Title = "VoidSharp TEST";

        TextEntry textEntry = Add("DTextEntry").Cast<TextEntry>();
        textEntry.Dock(DockType.Top);
        textEntry.DockMargin(50, 50, 50, 10);
        textEntry.Height = 40;

        textEntry.UpdateOnType = true;
        
        textEntry.OnValueChange((self, str) =>
        {
            Title = str;
        });

        ColorMixer mixer = Add("DColorMixer").Cast<ColorMixer>();
        mixer.Dock(DockType.Top);
        mixer.DockMargin(50, 10, 50, 10);
        mixer.Height = 150;

        Button button = Add("DButton").Cast<Button>();
        button.Dock(DockType.Top);
        button.DockMargin(100, 20, 100, 40);
        button.Height = 40;
        button.Text = "Click me!!";

        button.DoClick(() =>
        {
            button.Text = "You clicked me!";
        });
    }
}

This code is then transpiled by the CSharp.lua compiler into Lua code. The code is ugly, but still readable (but working in Garry’s Mod!)

-- Generated by CSharp.lua Compiler
VoidSharpTestAutoRefreshFunc()
local System = System
local VoidSharpDerma = VoidSharp.Derma
local VoidSharpNetworking = VoidSharp.Networking
local VoidSharpTestModels
System.import(function (out)
  VoidSharpTestModels = VoidSharpTest.Models
end)
System.namespace("VoidSharpTest.UI", function (namespace)
  namespace.class("MainPanel", function (namespace)
    local SendDataToServer, __ctor__
    __ctor__ = function (this)
  System.base(this).__ctor__(this)
  this:setTitle("VoidSharp TEST")

  local textEntry = this:Add("DTextEntry"):Cast(VoidSharpDerma.TextEntry)
  textEntry:Dock(4 --[[DockType.Top]])
  textEntry:DockMargin(50, 50, 50, 10)
  textEntry:setHeight(40)

  textEntry:setUpdateOnType(true)

  textEntry:OnValueChange(function (self, str)
    this:setTitle(str)
  end)

  local mixer = this:Add("DColorMixer"):Cast(VoidSharpDerma.ColorMixer)
  mixer:Dock(4 --[[DockType.Top]])
  mixer:DockMargin(50, 10, 50, 10)
  mixer:setHeight(150)

  local button = this:Add("DButton"):Cast(VoidSharpDerma.Button)
  button:Dock(4 --[[DockType.Top]])
  button:DockMargin(100, 20, 100, 40)
  button:setHeight(40)
  button:setText("Click me!!")

  button:DoClick(function ()
    button:setText("You clicked me!")
  end)
end
return {
  base = function (out)
    return {
      out.VoidSharp.Derma.Frame
    }
  end,
  __ctor__ = __ctor__,
  __metadata__ = function (out)
    return {
      methods = {
        { ".ctor", 0x6, nil },
      },
      class = { 0x6 }
    }
  end
}
end)
end)

This however just creates the custom panel for the UI. We need to show the UI somehow to the player. Let’s use a console command to do that:

[ConCommand("voidsharptest_ui")]
public void OpenMenu(Player ply, string s, string[] args, string argStr)
{
     MainPanel panel = new MainPanel();
     panel.SetSize(400, 400);
     panel.MakePopup();
      
     panel.Center();
}

After we type voidsharptest_ui in our client’s console, we can see the UI we made purely in C#!
Not that beautiful, but we can style that later.

But that’s not everything. We can use hooks, RPCs (net messages), save data into databases, etc…
Here are a few examples, you can view all at the example github link above.

This notifies all players that someone died

    [Hook("PlayerDeath")]
    public void OnPlayerDeath(dynamic gVictim, dynamic gInflictor, dynamic gAttacker)
    {
        // The hook gets called with gmod entities, we need to convert them to VoidSharp entities so we have OOP advantages
        Player victim = (Player) new Entity(gVictim);

        foreach (var ply in Players.GetAll()) {
              ply.ChatPrint(victim.Nick + " died!");
        }
    }

Sending net messages:

Sending side:

var data = new InventoryItem
{
      CreatedAt = DateTime.Now,
      PlayerName = "SMG",
      SteamId64 = player.SteamId64
};
            
RPC.SendToClient<InventoryItem>(player, "ReceiveInventoryRPC", data);

Receiving side:

    [RPC]
    public void ReceiveInventoryRPC(InventoryItem item)
    {
        var player = Player.LocalPlayer();
        player.ChatPrint("Got some useful data!");
        player.ChatPrint(item.PlayerName);
        player.ChatPrint(item.CreatedAt.ToString());
    }

Every object you send is automatically serialized, so you don’t need to worry about that.

Saving data to databases:

Class we wanna save:

[Table("mytable_name")]
public class InventoryItem
{
    [PrimaryKey]
    [AutoIncrement]
    public int Id { get; set; }
    
    public string SteamId64 { get; set; }
    public string PlayerName { get; set; }
    public Color PlayerColor { get; set; }
    
    public DateTime CreatedAt { get; set; }
}

Actually inserting it to the database:

var item = new InventoryItem
{
     PlayerColor = networkData.Color,
     PlayerName = networkData.Name,
     CreatedAt = DateTime.Now,
     SteamId64 = player.SteamId64
};

Database.Insert<InventoryItem>(item).Execute();

Fetching data from the database:

InventoryItem selectedItem = await Database.Select<InventoryItem>()
    .Where("SteamId64", "=", "76561198152492642")
    .Single;

There’s much more, but I can’t be arsed to show more. Just look here GitHub - VoidTeam1/voidcsharp: A Garry's Mod C# abstraction layer.

13 Likes

Cool project no doubt, but its hard to see the benefits of it all.

That being said, its still a cool ass project.

2 Likes

I don’t know their project, but I’m quite sure this isn’t a little project :sweat_smile:

@m0uka @Menschlich Good job!

1 Like

Cool concept, I can’t see a good use case in production as I expect it’ll produce quite un-optimized code. As is the nature of transpiled languages without that being their primary goal.

1 Like

Regardless of performance this is really dope. Seeing that shorthand code for RPC and panel callbacks warms my heart. :relieved:

1 Like

This is really cool! Is there any way to hot reload?

1 Like

If you configure everything correctly, then yes:

First you have to put this comment in all files you want to auto-refresh to call the autorefresh function (if you have changed the function in the Lua loader, you need to change this too, in the case of VoidSharpTest it’s this:)

/*
[[
    VoidSharpTestAutoRefreshFunc()
]]
*/

and then in Program.cs, you need to have a static void method called HandleAutoRefresh(), from there you can basically reload your code

1 Like