LPC-like functionality using C#
(2018/01/29)

This blog post will cover my implementation of LPC in C# for a MUD Engine I’m working on. The implementation is more of faux-LPC because I just sort-of follow the ‘idea’ behind LPC and not the specifics.

Basically LPC is interesting to me because it treats everything in the game as a file; the items, the rooms, the players, etc. You are assigned a login to both the game and an FTP server, you upload files to your FTP folder and view them in-game instantly, for the most part.  I’m not going to get into super-specifics but that’s the general idea. I wanted admins of my MUD to have the same ability but I wanted to use C# as the back-end.  Thankfully, C# loves reflection and so do I.  So here’s how I did it.

 

The first place my implementation would have to be completely different was the file-style itself. In LPC the objects/rooms/etc are basically C style classes.  I didn’t want to subject other admins to the pain of learning an entire programming language if they just wanted to ‘make’ something, so I came up with my own scripting language that essentially wraps complex functionality up and handles it during processing.  Here’s a small example.

 

inherits lib.std.Room

Name = A small room.
ID = 0
Coordinates = 0 0 0
ShortDescription = A small room.
LongDescription = This is small room in a small place.
LightLevel = 1

addaction do_echo echo
addexit locations/test/test2.txt east
addproperty street street

startcustom
static int fired = 0;
public static void HelloWorld(params object[] parameters)
{
     Player p = (Player)parameters[0];
     if (p != null) {
          fired++;
     p.WriteLine("I have fired " + fired + " times.");
     }
}
endcustom
addcaction HelloWorld hiworld

This doesn’t show off all of the functionality or the keywords. It is just the basics and it’s pretty simple.

Here’s a breakdown of what is going on:

    • Line 1: The first line tells the system what base class this new object will derive from. In this case it’s a Room.
    • Lines 3-8: Notice that the following block of parameters are all capitalized. They are direct 1:1 parameters of the Room class. Setting them here will set them directly for the new Room we’re creating.
    • Line 10: addaction creates an action called ‘echo’ for the player. Typing ‘echo’ in this room will activate a system-action called do_echo that is stored in the player commands.
    • Line 11: addexit creates an exit from this room to another room. The first parameter is the room’s file location (even if it doesn’t exist yet) followed by the command used to access this exit. Multiple names are allowed via comma separator.
    • Line 12: addproperty adds a property to this room, in this case ‘street’. This property is used by, say, the NPCs who’s job it is to walk the streets and pick up/destroy trash items.  Properties are pretty much arbitrary.
    • Lines 14-24: Starting with startcustom and ending with endcustom these lines contain any and all custom c# code this room requires.  In this case, the room has a static variable that counts how many times the command has fired and repeats it to the player when activated. You’ll notice that the incoming parameters are a params object[], this is because these functions are given an insane amount of internal data, so they are extremely powerful. This is literally the most simple example of their capabilities. As you can see, the first object in said array is the player who fired the function.
    • Line 26: addcaction adds a CUSTOM action to the room’s action list. It binds ‘hiworld’ to the HelloWorld function, allowing anyone who’s in this room to fire said function by using said command.

Since this is unreleased code and the engine isn’t live yet, there are probably some security issues that will be delt with at a later date. Also, there’s a fair amount of error checking I’ll need to do that isn’t there. All that said, the information is useful so if you’re able to do something with it, great. If you figure out a better/more secure way of doing anything then let me know.

Long story short, the files are read in and each line is analyzed for specific syntax.  The first line’s keyword is ‘inherits’.

When inheriting, I check the rest of the string against the actual namespaces inside my project.  The room, and most other base classes, are inside the namespace ‘lib.std’.

string workingNamespace = line.Replace("inherits", "").Trim();
workingNamespace = typeof(Server).Namespace + "." + workingNamespace;

string workingClass = line.Substring(line.LastIndexOf(".") + 1);
workingNamespace = workingNamespace.Substring(0, workingNamespace.LastIndexOf("."));

Type resultObject = AppDomain.CurrentDomain.GetAssemblies().SelectMany(t => t.GetTypes()).Where(t => t.IsClass && t.Namespace == workingNamespace && t.Name == workingClass).First();

workingObject = Activator.CreateInstance(resultObject);

workingObject is the generic Object that contains whatever it is we’re creating. From this point on I can use: if (workingObject is WHATEVER_CLASS) { //DO STUFF; } to test/access things.

To handle the “=” lines, it’s pretty simple. Parse each side of the line and grab the Property Name and Property Value.  Then, it’s just a matter of:

if (workingObject.GetType() != typeof(object))
{
     Type workingObjectType = workingObject.GetType();

     PropertyInfo pi = workingObjectType.GetProperty(PropertyName);
     if (pi != null)
     {
          pi.SetValue(workingObject, TypeDescriptor.GetConverter(pi.PropertyType).ConvertFrom(PropertyValue));
     }
     else
     {
          retstr = "Property Name ("+PropertyName+")Invalid - Check Capitalization and Spelling";
     }
}

retstr is just a returnstring I use to display any issues to the admin-player during room/object refresh. As you can see, Capitalization and Spelling matter 🙂

For things like addaction, the binding is also simple, parsing out the function and the command word, then adding them to the rooms action list. I won’t go over that here.  What you -are- probably interested in, is how the custom code is compiled in and the addcaction command finds it.

StartCustom/EndCustom

The first thing you do is make sure you’re inside the startcustom/endcustom codeblock. Then take every line of code and compile it together into a string.  One thing to note, you cannot use single-line comments here. Shit breaks since the string is a single line string when parsed. You were warned.  Once you have said string you can then wrap it with your allowed using statements. You can also run through the code first and strip out malicious stuff, like using statements. Your call. Depends on how much trust you’ll be giving to people allowed to input this code to begin with. Not a topic of discussion here.  You’ll take the code and wrap it in a set of using statements and a namespace, like so: CustomCode = "using System;using MUDENGINE.lib.std;using MUDENGINE;namespace CustomFunctions{public class CustomFunction {" + CustomCode + " }}";

After all that is said and done, here’s where the magic happens:

CSharpCodeProvider provider = new CSharpCodeProvider();
CompilerParameters cp = new CompilerParameters();
cp.GenerateExecutable = false;
cp.IncludeDebugInformation = false;
cp.GenerateInMemory = true;
cp.TempFiles = new TempFileCollection("tempfiles/", false); //Disabled because everything is in memory unless I'm debugging.
cp.ReferencedAssemblies.Add("System.dll");
cp.ReferencedAssemblies.Add("System.Core.dll");
cp.ReferencedAssemblies.Add("CORE_MUD_ENGINE.dll");
CompilerResults results = provider.CompileAssemblyFromSource(cp, CustomCode);

foreach (CompilerError ce in results.Errors)
{
     if (ce.IsWarning) continue;
     string ErrorLine = String.Format("{0}({1},{2}: error {3}: {4}", ce.FileName, ce.Line, ce.Column, ce.ErrorNumber, ce.ErrorText);
     if (p != null && p.IsAdmin)
          p.WriteLine("<red>" + ErrorLine + "<reset>");
     Logging.Log.Error(ErrorLine);
     break;
}

//SKIPPING STUFF THATS NOT RELEVANT, SORRY!

if (workingObject is Room)
{
     Type customFunction = results.CompiledAssembly.GetType("CustomFunctions.CustomFunction");
     List customMethods = customFunction.GetMethods().Where(method => method.Attributes.HasFlag(MethodAttributes.Virtual) == false
          && method.Attributes.HasFlag(MethodAttributes.Static) == true
          ).ToList();

     customMethods.ForEach(methodinfo =>
     {
          ((Room)workingObject).Methods.Add(methodinfo, String.Empty);
     });
}

This code sets up a CSharpCode provider as well as compiler parameters that include references to the DLL our engine resides in. We compile the snippet, outputing errors and moving on if there’s a problem. Next we get a list of all custom methods inside this room and add them to a list the room itself can access.  You’ll use this to bind the addcaction  command, like so:

((Room)workingObject).Methods[((Room)workingObject).Methods.First(mt => mt.Key.Name == function).Key] = command;

The engine will now find this method when the command word is called inside the room and fire it off as if it were completely internal. There is an internal admin command that will refresh any filepath given to it, which will hot-reload the file back into the system.

Leave a comment

Your email address will not be published. Required fields are marked *