1. In this thread I'd like to discuss the aspects of porting plugins between games because of the newly added feature to be able to publish plugins as universal, because I suspect that most people writing universal plugins will not have any experience in regards to porting code between platforms (mainly because of the languages Oxide supports - C# (and the closely connected Java world), Javascript, Python and Lua are all languages that mostly take the burden of portability off your back.
    Most people probably won't have to care about this, but for those that care about writing good and clean portable code it's maybe worth a read. This is especially relevant for people that want to port huge and complex plugins.

    First off, I think universal plugins are a great idea.
    I can see two different kinds of plugins being published here:
    • Plugins that are inherently portable because they only work with Oxide's interface (e.g. Covalence)
    • Plugins that are not inherently portable and need specific code for each platform
    The first kind isn't problematic at all, this is what universal plugins are great for and there's no need to discuss this in detail. Strive to replace platform specific code with code that uses a portable Oxide equivalent, the existing universal plugins published today are mostly decent examples of cases where you can accomplish this.
    The second kind is a bit more complicated.

    A plugin that isn't portable generally consists of code that is "sort of" portable, for instance logic code, and code that is specific to the platform, for instance hooks.
    When I say "sort of" portable I mean that in general this code should be portable, but because some hooks lack on some platforms and different things being possible on different platforms, you might actually have to add platform specific code to code that should in theory be portable, meaning that sometimes platform independent code may turn into platform dependent code.
    There are essentially two different kinds of platform specific code:
    • Platform specific code at the top of your function callstack (hooks)
    • Platform specific code at the bottom of your function callstack ("sort of" portable code, code that deals with game state)
    Both need to be treated differently.

    There are three different ways to port a plugin between games:
    • Duplicate code (create a single plugin for every game you're porting to)
    • Use preprocessor directives for specific games (e.g. #if Rust)
    • Transfer portable code into an API plugin (publish API plugin as universal plugin, publish platform specific plugins that use the API plugin as game specific plugins)
    Each of these has advantages and disadvantages and it's important that one understands these when deciding how to port his plugin.

    Transfer portable code into an API plugin:
    • Introduces an extra dependency, which is always annoying for anyone who wants to use your plugin
    • Clearly seperates portable code from non-portable code
    • Clearly seperates a plugin from an API
    • Becomes messy when portable code turns into "sort of" portable code
    Use preprocessor directives for specific games:
    • Easily results in strong coupling between portable and non-portable code
    • Becomes very messy when not careful
    • Allows for lots of flexibility in regards to avoiding duplicate code
    Duplicate code:
    • No coupling between portable and non-portable code
    • Requires more effort to update portable code
    • Allows for lots of flexibility in regards to platform specific features
    Each of these is useful in different kinds of situations:
    • Transfer portable code into an API plugin when the code you're transfering will be useful to lots of plugins and the code you're dealing with isn't "sort of" portable.
    • Use preprocessor directives for specific games when the code you're required to write to port your plugin isn't that complex.
      C devs have been porting code like this for decades and in many cases the code was an absolute mess (just take a look at the GNU coreutils - yuck!).
      https://www.usenix.org/legacy/publications/library/proceedings/sa92/spencer.pdf is a paper published in 1992 that raises some interesting points in regards to porting code in C and is definetly worth a read.
      We can transfer that knowledge to Oxide with some adjustments.
      Above I mentioned that there is platform specific code at the top of your callstack and platform specific code at the bottom of your callstack. In C you're mostly not dealing with any kind of hooks, so almost all code is platform specific code at the bottom of your callstack, while in Oxide you're dealing with this sort of code on a daily basis.
      The paper states that platform specific code should be clearly encapsulated somewhere and not spread all over the place. In Oxide we cannot do this on a per-file basis (only in the case of using an API plugin!), but we can do this on a per-function or a per-class basis.
      The best approach in this regard is to encapsulate your platform specific code in a set of simple functions instead of spreading it all over the place.
      For code at the top of your callstack we'll have to invert this process: Obviously when our platform specific code is the name of the hook we're using, we can't encapsulate that hook in any way. What we'll have to do is encapsulate our portable code and our non-portable code at the bottom of the callstack in some manner and call that code from our platform specific at the top of the callstack - the process is inverted.
      In general that means that there should be non-portable code at the top of the callstack, that portable and non-portable code at the bottom of the callstack should be encapsulated and that the non-portable code at the bottom of the callstack should be encapsulated once more to decouple it from the platform independent code.
    • Duplicate code when the code you're required to port is very complex.
      When you find that you can't easily encapsulate non-portable code as mentioned above without your code turning into a complete mess because of preprocessor directives being all over the place (or even being nested!) then you should consider publishing seperate versions of your plugin, even if that means duplicating some code. Duplicating code isn't that bad, even if that's often taught. Google once had a policy to avoid any duplicate code and they're significantly suffering from that policy today and trying to revert huge chunks of it. Coupling and code repetition are mostly inverse to each other and deciding which degree of coupling and code repetition is the correct amount requires a bunch of experience.

    This knowledge is not local to Oxide!
    Need to port your program to a different OS or a different processor architecture? This is still relevant!

    I hope this helps those that want to port bigger things, I know there are quite some *huge* plugins on Oxide that might benefit from this.
    Also, I know that some of this might be pretty complicated - if you've got suggestions on how to simplify it, just let me know.
     
  2. Wulf

    Wulf Community Admin

    Quite the long read!

    Covalence is what would eventually be used to replace all other API available via Oxide as well as most direct calls to game code. Most of my plugins I try to do as much as possible to use the Covalence API, though most of my plugins are also generally pretty simple and game-agnostic.

    For the plugins that are using preprocessor references, that code would eventually be replaced by the Covalence API once it wraps that functionality. Right now Covalence is pretty basic, but it is expanding slowly yet surely. I'll be working on it more as I get time, wrapping more hooks and adding more to the IPlayer and ILivePlayer method. Things are bound to break though while it gets settled into what we want it to be.

    It's also worth noting that only Rust, Rust Legacy, Reign of Kings, Hurtworld, and Hide & Hold Out are currently using Covalence. The other games will be supported as I get time.
     
  3. That's great, although I'm sure Covalence will never completly replace game specific API, for multiple obvious reasons.
    Different games simply often work differently. Some games may allow you to fire a specific hook that others will never allow you to fire, requiring you to provide different arguments as well.
    Trying to unify *everything* will eventually result in a fairly awkward API.

    The idea to unify things almost all games have in common is awesome, though.

    Also, this post is posted here because I didn't really find a better place to give tips for universal plugins. When there's a place for that kind of stuff it'd be nice if you could move it.
     
  4. Wulf

    Wulf Community Admin

    Covalence will eventually replace all of the currently used API from Oxide, and the goal is to provide as much as possible for what people use directly from games as well. Yes, some of the games do work differently, but that's something we try to handle in the wrapper to account for the differences. Some will likely never be available for all games, but for the most part we can wrap quite a bit. The plugins may have to check if some things exist before handling them, but we'll see.
     
  5. Well, in regards to having to check if some things exist before handling them, that's what I meant with a fairly awkward API.
    I don't think a unified API that requires you to check whether certain things exist (almost as if you were using loads of flags like WinAPI does) is better than just duplicating some code for different plugins.
    Unifying *everything* is roughly the equivalent of the following:
    You're combining a function foo(int) and a function bar(int, int) into a function foobar(int, int), where the second parameter is optional and the behaviour changes depending on whether the second parameter is present or not.
    In some cases this makes sense, at a large scale it becomes cruft and rather annoying.
    When executed at a larger scale, this is also known as "the wrong abstraction" (explained here for instance: The Wrong Abstraction).
    John Carmack also talked about this (to a more extreme degree, though, even suggesting to trash abstractions without flags when possible) in 2007: Jonathan Blow's home page
    On a plugin-level (not at an Oxide-level ;)), this is what I was getting at when suggesting to duplicate code when porting plugins in some cases.
    When an API with too many flags (or too many preprocessor directives, which is just about the same, just in a different coat) becomes too complex, duplication yields many advantages, the greatest one being simplicity.
     
  6. Wulf

    Wulf Community Admin

    We'll see where it goes and how it turns out I guess. There's still a long ways to go.