Code organization using includes, not dependencies (other plugins)

I'm accustomed to using includes to organize classes into single managable files.

I'm trying to apply this to my plugin which has over 10k lines of code.

My current file structure is:

  • plugins/PluginName.cs
  • plugins/include/PluginName/Classes/ClassFile.cs
  • plugins/include/PluginName/Dictionaries/DictionaryFile.cs
  • plugins/include/PluginName/Views/ViewFile.cs
I did try using the current example in the docs for "// Requires:" but I get the error that the plugin doesn't exist, well of course not, because it's not a plugin, it's an include file, plugins/include/PluginName/Classes/ClassFile.cs, so I attempted to use "// Require: PluginName.Classes.ClassFile" dropping the "s" from Requires.

When I use "// Require: PluginName.Classes.ClassFile" I think it's accessing the include folder, but still I'm getting a namespace error even when declaring the class as public like I read in a post on oxidemod.org from 2017.

Has include functionality changed since the 2017 posts I was reading? (here)
Do we need to explicitly declare the class namespace within the class file, within the root plugin file, maybe both?
Can we include Dictionary and view/UI related code in isolated files without declaring a namespace for them? It seems that should be possible.

Once I get this figured out I would like to contribute to the documentation with detailed examples regarding includes.

The Requires functionality only works with plugins, not includes. It was never completed though, so isn't really fully supported. The idea of it was that you could reference public methods and such in another plugin directly, but only public.

I read a comment you wrote here.

In that post you say "Yes, you can split into as many files as you'd like. You can have Ext.Name.cs files under the oxide/plugins/include folder that plugins can reference, you can // Require: PluginName at the top of your fine and add using statements for referencing methods directly, or you can use the plugin.Call method like how you saw in the Docs."

So, how do I split my plugin into multiple class files for proper organization as outlined in the Approval Guide?

I have written over 10k lines and will not release them in a single .cs file because it would reflect poorly on my organizational skills, which I have an ernest interest in keeping intact.

Plugins are only single files, and the Requires is only for plugins, not includes. I believe you most understood that other post, it was a list of methods for referencing other files.

You can have one plugin that contains all shared methods and then use // Requires for accessing public methods or .Call for private methods, or you can use include files, or you can make an extension.

Explain to me like I don't know anything at all.

In php I use:
require_once "path/to/classes/classfile.php";
There is no complication, the class file simply loads and the class works within the scope it is included/required.

In C#/Oxide how would I include/require without getting "The type or namespace name `classfile' could not be found. " or "plugin does not exist" errors?

I've been searching google for 3 days and everything I see in videos and sample code that I copy paste into C#/Oxide will not compile. Every working example that will compile in the Visual Stuidio debugger will not compile within Oxide.

Hey Nuzy.

Thank you for the feedback! We recently had a discussion in the community (discord) about this very subject.

It is unlikely that something like this would end up in Oxide because Oxide can't be unit tested and we can't guarantee any major feature changes will remain stable (Oxide changes are immediately pushed to production).

However, I think something along these lines would be a great addition for uMod, which has a considerably more advanced compiler than Oxide.

Firstly, let me explain how this works in uMod because this system is fairly complex. We have 2 different types of dependencies (Optional and Requires) and each type of dependency has two implementations (Implicit and Explicit). As a result, we have 4 different dependency implementations: Implicit Optional (default), Explicit Optional (+duck typing), Implicit Requires, and Explicit Requires (like //Requires but more stable). More information can be found here.

Since your experience is with PHP, I'll explain some of the differences from that perspective. In PHP, any class that's autoloaded is loaded globally and can reference any other part of the system regardless of the order the class was loaded. This is not true in C#, every time a class is loaded, all of it's dependencies must be compiled with the class.

For example, if PluginA depends on PluginB and PluginB is loaded later (after the first load), then PluginA must be unloaded and recompiled with PluginB. This is a implementation detail specific to C# and how it handles assemblies. It is also what makes the compiler an "incremental compiler." Yes, technically there are some shortcuts around this in some cases, but generally this cannot be avoided.

At any rate, the current uMod compiler already does something similar to auto-loading in a limited capacity. Namely, type promises will keep track of missing types not found when a plugin compiles. If any subsequent compilations fulfill the type promise, then both plugins are re-compiled together. The limitation is that this functionality only works on plugins in the plugins directory (not sub-directories).

The uMod compiler adds a couple of new dependency options (as previously mentioned) which are unavailable in Oxide, making plugin integrations more flexible and performant in general.  As of right now, we have explored several options to improve plugin organization with dependencies beyond what we've already done.

Some options being considered...

PHP PSR-4

Autoloading similar to PSR-4 (PHP) which creates a convention where there is a one-to-one match between namespaces and the directory structure. For example uMod.Plugins.Calytic.MyClass would be in umod/plugins/Calytic/MyClass.cs.

PROS:
  • Code remains easily accessible via .cs files
  • Code can be split up into multiple files fairly simply (with matching namespace/directory convention)

CONS:
  • Having plugins strewn about multiple files in the umod/plugins directory may lead to confusion, where users move files around where they aren't supposed to be. The plugins directory isn't 100% comparable with the /vendor directory that composer uses because vendor files are never supposed to be modified, whereas users routinely find themselves modifying/moving files in the plugins directory.
  • C# does not allow a class and a namespace to have the same name in the same namespace. For example, you can't have "uMod.Plugins.MyPlugin" (MyPlugin.cs) and "uMod.Plugins.MyPlugin.OtherClass" (MyPlugin/OtherClass.cs)

JAVA .jar

PROS:
  • Plugins are self-contained packages, like ZIP files, where all of the files related to the plugin are in a single place (like MyPlugin.umod)

CONS:
  • Code no longer easily accessible via .cs files (users must understand that ".umod" files are an archive and extract them to view contents)
  • Code in ".umod" cannot easily be modified, it would require developers to extract, modify, and recompress the umod file every change.

There may be more options, and if anyone has a suggestions please feel free to chime in.  Implementing either thing will require sweeping changes to a wide variety of systems, both in uMod and other related systems (like this website). As such it's not something that's likely to happen quickly but it's good to get the ball rolling..

Thank you Wulf, and Calytic, for your replies to this subject. I truley appreciate this detailed insight.

I was completely confused as to why .resx files were not working as expected, and my sub-foldered class files were being ignored. Now I mostly understand.

In addition to PHP and it's compliment Javascript, I have a lot of C++ experience and some Java experience. C# is something I've only used sparingly.

I like the idea of being able to organize large plugins (source). The problems seem to stem from C# namespaces within umod/oxide. As I understand, namespace is just a pseudonym for syntactical file path. Please correct me if I'm wrong.

Jar style containers slow daily progress IMHO. Using PSR-4 could be ideal. In that scenario I think it would be best to only allow folders within the umod/plugins/ directory. Containing all the source files within the plugin's own folder (i.e., umod/plugins/PluginName/) would reduce confusion when adding or removing a plugin. However, if namespaces are going to ruin that, maybe there's no hope for organization beond regions in a single file.

Maybe using a precompiler could help? Some functionality that introduces an "include" keyword. Precompile the class files into a single file before compilation. Suppose you could write: include "Classes/ClassName.cs";, a relative path inside your plugin's Main.cs, then the umod precompiler would just insert that class file into a PluginName.cs (like a string concatenation) before compiling with the umod/C# compiler. I've done similar things before for use in web apps and ARM programming.

File structure something like this:
umod/plugins/PluginName/Main.cs
umod/plugins/PluginName/Classes/Object1.cs
umod/plugins/PluginName/Classes/Object2.cs
umod/plugins/PluginName/Classes/Object3.cs
umod/plugins/PluginName/Resources/Localization.resx (or xml?)
umod/plugins/PluginName/Resources/Localization.US_EN.resx
umod/plugins/PluginName/Resources/Localization.EU_FR.resx

Within Main.cs we would create our plugin namespace, plugin Object, and include the child ObjectX.cs files in the suitable order, and reference the Localization.X.resx files. The precompiler would simply assemble a file like PluginName.cs by concatenation and send it to the C# compiler. There would be no need to do sweeping changes across the umod extension itself because the precompiler would feed it what it already wants.

Does that seem like a viable approach? My lack of experience in C# is a bit debilitating in these discussions, but I'm picking up on things failrly well. Please point out my mistakes so I can learn better.

What are everyone else's thoughts on this?

eU2udFWKnhuuYNQ.png Nuzy

The problems seem to stem from C# namespaces within umod/oxide. As I understand, namespace is just a pseudonym for syntactical file path. Please correct me if I'm wrong.

Nope. Technically you can structure a project using this convention (for the most part), but C# does not require the convention and technically classes and namespaces can be located anywhere in a project. The important part is the code, the file/directory structure is largely irrelevant.

Adding "includes" is a decent option. It would probably be the easiest and the most flexible approach. Essentially a plugin becomes a "project" and using some type of annotation we can fairly easily inform the compiler to accompany the plugin compilation with it's "includes". The only real problem is that without some organizing conventions, we run the risk of having developers create a real mess, each using their own pathing and separation.

That said, whatever approach we take, I want to put an emphasis on doing it Right(tm) out of the gate. It's likely that whatever we do here, we will be stuck with for awhile. Even if we change it later, whatever implementation we had will linger and may represent a barrier to further improvement.

I'll setup a small 2-3 file project to figure out this namespace thing. Making my first attemps at C# organization with a large project was likely too ambitious.

I completely agree with this...

UhsD79STEOsDKvb.png Calytic
That said, whatever approach we take, I want to put an emphasis on doing it Right(tm) out of the gate. It's likely that whatever we do here, we will be stuck with for awhile. Even if we change it later, whatever implementation we had will linger and may represent a barrier to further improvement.

In my experience, even if a newer way is easier, better, and more efficient for the target user group they will tend to fall back on previous standards (habbits) or procedures.

The old addage "keep it simple" applies here also. So I understand why I'm seeing 20k line plugins in the same file in these repos. It's just a very large pain sometimes, even for the person who wrote the original code, to find things after the file gets that large.

One of my dreads is opening a file to see no commenting whatsoever. It takes a few minutes to write good comments for a method, and possibly over an hour to figure out an uncommented method. The sparse time saved by not writing comments pales to time spent desiphering uncommented code. This is especially true when you have many thousand lines in the same file.

Is the Oxide->uMod migration on github? All I could find there was the Oxide.Core repo and it's adjacent game specific repos.

Oxide->uMod migration is fairly trivial in most cases. uMod will copy/rename some data files, and in the case of plugins it will do some replacements before sending an Oxide plugin to the uMod compiler.