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:
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.
- 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 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:
Both need to be treated differently.
- 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)
There are three different ways to port a plugin between games:
Each of these has advantages and disadvantages and it's important that one understands these when deciding how to port his plugin.
- 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)
Transfer portable code into an API plugin:
Use preprocessor directives for specific games:
- 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
Duplicate code:
- 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
Each of these is useful in different kinds of situations:
- 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
- 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.
Considerations in regards to universal plugins
Discussion in 'Feature Suggestions' started by sqroot, May 20, 2016.
-
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. -
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. -
Wulf Community Admin
-
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. -
Wulf Community Admin