HomeUser Control Panel (unavailable in archive)ForumsTutorialsArt GalleryResourcesMaps

[Advanced] CLI injection in WE using Grimoire

06-08-2008, 09:09 PM#1
Ion
EDIT: Total revamp of the old post since it's all obsolete by the new version.

I've created a couple of loaders that makes it possible to inject .NET assemblies into the World Editor using Grimoire.

This works by injecting a native win32 library which loads the "CLI loader" that in turn loads your .NET assembly.
The reason why we need two loaders is simple. A .NET assembly doesn't have any entry-points. Grimoire can inject it, but it won't execute since the system doesn't know which function to call. The PELoader calls the "load" function in the CLILoader, which in turn loads every assembly it can find that implements the IWePlugin interface. The second loader eliminates the need for exports in the target assembly, since CLI-assemblies doesn't export any functions by default.

UPDATE: A plugin architecture has been implemented (thanks to BlacKDicK). This approach requires your plugin to inherit the IWePlugin interface (by referencing Shared.dll). I guess plugin-creation has never been easier.. Below is a short example (also included in the archive).

(pluginTest.dll)
Code:
using System;
using System.Collections.Generic;
using System.Text;
using Shared;
using System.Windows.Forms;

namespace pluginTest
{
    public class MyCoolPlugin : IWePlugin
    {
        private Version version;
        private String author;
        private IWeNotify notifier;
        public String PluginAuthor { get { return author; } }
        public Version PluginVersion { get { return version; } }
        public IWeNotify Notifier { get { return notifier; } }

        public MyCoolPlugin()
        {
            //set some properties
            author = "Risc";
            version = new Version("1.0");
        }

        public IWeNotify RegisterNotify()
        {
            notifier = new PluginNotifier();
            notifier.OnRegisterPlugin += new delegateRegisterPlugin(notifier_OnRegisterPlugin);
            notifier.WindowCreated += new delegateWindowCreated(notifier_WindowCreated);
            //the return-type is reserved for further use.
            return notifier;
        }

        void notifier_WindowCreated(string className, string windowText, IntPtr hWnd)
        {
            if(windowText == "About Warcraft III World Editor UMS")
                MessageBox.Show("Cool");
        }

        void notifier_OnRegisterPlugin()
        {
            MessageBox.Show("Hello WorldEdit!");
        }
    }
}

To use the PELoader, you'll need to reference it in "we.conf.lua":
Code:
-- CLILoader
loaddll("bin\\PELoader.dll")

CLILoader.dll, PELoader.dll and all plugins that should be loaded must reside inside Grimoire's bin-folder.
Shared.dll needs to reside in your Warcraft III folder (due to pathing).

Please submit any bugs/suggestions you can find.
Happy coding!

//Ion
(This was originally submitted @TheHive where I'm known as Risc because "Ion" was already taken when I registered :P)
06-08-2008, 11:44 PM#2
Vexorian
I guess filling grimoire with .net stuff was required to ensure it was even less portable.
06-09-2008, 10:26 AM#3
Ion
It's still portable via the Mono Framework.
06-09-2008, 07:06 PM#4
PitzerMike
Since Grimoire isn't portable the argument is irrelevant anyway.
06-09-2008, 07:50 PM#5
Vexorian
Quote:
Originally Posted by Ion
It's still portable via the Mono Framework.
when you mix grimoire's winapi requirement with this .net lock-in you get something that can only hope to ever work in windows, no, Mono doesn't help, you would need a WINE-Mono hybrid, which does not exist. I guess that's MS' objective with things like XNA so making grimoire mimic such unportable nightmare makes me quite mad.

Quote:
Since Grimoire isn't portable the argument is irrelevant anyway.
I just hope people don't use it, cause when grimoire becomes able to work in WINE this will still remain unportable. It is not like people would die if they don't use .net for god's sake.
06-09-2008, 10:25 PM#6
PitzerMike
Shouldn't you be able to install the .NET framework on a Windows emulator like WINE?
06-09-2008, 10:50 PM#7
Ion
Well I'll guess it's up to the developers to decide if they want to make their plugins cross-platform or not.

I released this because I think developers are smart enough to realise that their .NET assemblies won't be able to run in a linux/mac environment and thus hindering their popularity.

Use this only if you (like me) disagrees with Vexorian's argument.
06-10-2008, 12:51 AM#8
Ion
Quote:
Originally Posted by Ion
Well I'll guess it's up to the developers to decide if they want to make their plugins cross-platform or not.

I released this because I think developers are smart enough to realise that their .NET assemblies won't be able to run in a linux/mac environment and thus hindering their popularity.

Use this only if you (like me) disagrees with Vexorian's argument.

EDIT: BTW, running something in WINE doesn't make it "portable".
06-10-2008, 07:26 PM#9
Vexorian
Yes it does as WINE is a different platform from window's winapi, it is not an emulator. You can focus development in your tools to make them work with WINE's implementation of winapi. Heck, It might not classify an strict definition of portable, but it is still aeons more portable than .net . Now that pipe made grimoire work in WINE, I hope people won't use this stuff, cause it will greatly reduce the quantity of places you would be able to run grimoire in for what I think is no good reason.

Also, grimoire's lua was already able to call apps of any type, so I don't think it makes sense to incur in such grimoire hacking to run .net code.

Quote:
Shouldn't you be able to install the .NET framework on a Windows emulator like WINE?
Oh sure but badly. Wine is not a windows emulator, it is an open source winapi (and directx, etc) clone. Correctly running .net there is very hard.
06-10-2008, 07:47 PM#10
Ion
EDIT: Fixed a minor typo.

Quote:
Also, grimoire's lua was already able to call apps of any type, so I don't think it makes sense to incur in such grimoire hacking to run .net code.

I guess you've never touched a .NET assembly before. A .NET DLL doesn't export any entry-points since there is no v-table fixup present. It is possible to export functions by disassembling the DLL to IL-code. There is no other way. This tool loads a DLL WITHOUT entry-points. If you read my first post, the PELoader loads the CLILoader (which is a .NET assembly). You can skip using the CLILoader, but that makes it necessary to make the proper adjustments to the DLL you want to inject (adding an export address table, a name pointer table, an ordinal table, an export name table and an export directory table). You would also need to recompile everything with an IL-assemblator.

Using this tool, you don't need to do any adjustments and can get to coding right away (this simpifies debugging alot!).

Whatever.
Use this tool if you want to make grimoire-plugins in .NET.

Have fun!
06-10-2008, 08:35 PM#11
BlacKDicK
Quote:
Please submit any bugs/suggestions you can find.
I have some suggestions:

1) What about using an interface instead of hardcoding the class "WEInit" and the method "static begin()" that the assembly developer must implement? Provide ppl an interface and let them do whatever they want, as long as they implement that interface. Inside your loader, scan the "bin" (or another folder) and load whatever plugins found that implement your interface. Reflection can help you.

2) I don't know why do you use "*.conf files". Properties / Settings (XML based) are the .Net way of storing that. Leave "*.conf" files for linux / java fans (no offense Vex).

3) You should take a look on delegates / events and "Marshal.GetDelegateForFunctionPointer". You can create a pretty cool callback system using it.

4) This is related to both #1 and #3. Create a simple interface, say "IWePlugin", give it 2 methods, RegisterPlugin() and UnregisterPlugin() and notify them (the plugins) by using delegates. I think you (not each plugin developer) should care about window hooks and whatsoever. Provided you start some event-based api, it should be easier to notify the plugins whenever something happens. Whenever your lib finds something usefull, let the subscriber-plugins know about it by firing the event, say "OnShowAboutBox()", for an example.
06-11-2008, 04:26 PM#12
Ion
Good idea BD..
Perhaps something like this:
Code:
using System;
using System.Collections.Generic;
using System.Text;

namespace NewCoolPlugin
{
    public interface IWePlugin
    {
        void RegisterPlugin();
        void UnRegisterPlugin();
    }

    public class Plugin : IWePlugin
    {
        public void RegisterPlugin()
        {
            //called whenever a plugin is loaded
            System.Windows.Forms.MessageBox.Show("Hello WE!");
        }

        public void UnRegisterPlugin()
        {
        }

        public static void CLILoad()
        {
            //create a new instance of this plugin
            Plugin p = new Plugin();
            //register the plugin
            p.RegisterPlugin();
        }
    }
}

It still requires a static method exposed to the loader so it can initialize the class tough.
This approach would make it easy to create multiple plugins within the same assembly. Pretty neat :)
06-11-2008, 05:09 PM#13
d07.RiV
Quote:
Originally Posted by PitzerMike
Shouldn't you be able to install the .NET framework on a Windows emulator like WINE?

WINE = Wine Is Not Emulator

Any hopes a third party WE is ever gona be made? I know a guy who's starting such a project and I don't actually see what the difficulties should be. Like, the most complex part I see is making the terrain editor look nice (that is, generate "rough" cliffs like WC3 does).
06-11-2008, 06:49 PM#14
BlacKDicK
Quote:
It still requires a static method exposed to the loader so it can initialize the class tough.
I am pretty sure that plugins could be done without the static method. See the code below. It is managed C++, but it can give you an idea.

Code:
// crapZilla.h

#pragma once

using namespace System;
using namespace System::Reflection;
using namespace System::IO;
using namespace System::Diagnostics;

struct __declspec(dllexport) CrapLoader
{
public:
	CrapLoader()
	{
		DirectoryInfo ^dirInfo = gcnew DirectoryInfo(
			Assembly::GetExecutingAssembly()->Location);

		if (dirInfo->Exists)
		{
			for each (FileInfo^ fileInfo in dirInfo->GetFiles("*.dll"))
			{
				try
				{
					Assembly ^currAssembly = Assembly::LoadFrom(fileInfo->DirectoryName);
					for each (Type^ exportedType in currAssembly->GetExportedTypes())
					{
						if (exportedType->IsInstanceOfType(crapZilla::IWePlugin::typeid))
						{
							crapZilla::IWePlugin ^plugin = (crapZilla::IWePlugin^) Activator::CreateInstance(exportedType);
							plugin->RegisterPlugin();
						}
					}
				} catch (Exception ^ex)
				{
					Debug::WriteLine(ex->Message);
				}
			}
		}
	}
};



Also, I'm curious how the hell does this "PELoader" works. I mean, we can't have any .NET code inside (or referenced by) the DllMain. Grimoire loads PELoader by calling DllMain and then PELoader loads CLILoader. My approach would be creating a standard C++ class (CLILoader) that wrapps the loading process of the plugins. I would expose that class by using standard dll export and would call it from the PELoader.

I got curious with the process you're currently using. As I said, loading .NET stuff in DllMain is not possible, so if you could explain your loading process I could help you somehow. I got interested in your idea and I think this could be a sweet addition to WE. Provided you have an working plugin system, it could be nice to have BLP->JPEG conversion embedded into WE. Also, we could attach a MpqExplorer-like interface, to make it easier to export, import stuff and maybe some other stuff, like BLP palleter, map optimizers, etc..

As for the ppl here that complain about using .NET and thus making it "not portable", I just don't get your point. WE is a Win32 app, it is not a GTK neither cocoa app. Ppl are starting to make a hassle just because Ion's idea "could make things worse for Wine". If Blizzard supplied us with a GTK version of it, that would make sense, but what we DO have is a Win32 version and that's it, nothing more, nothing less. Trying to make it work with Wine or MAC is to spend , much effort in a (somehow) useless journey, coz 99,9% of WE users are Win based.

I do like the idea to make it cross platform, but we can't forget that WE is Win32. Making it work with nix, by using Wine, Cedega, whatsoever are nothing more than "patches", sort of "band-ainds" to make it run. You guys are just making it harder and for what? To make (maybe) 20 ppl happy? I'm sorry, but I don't agree with that. You want a nix version of WE? Well, I guess this should be Blizzard's problem, not yours. Of course, it is just my opinion about it. I say we could have .NET plugins, and if they do work (or not) on nix or macs, that's another problem.

EDIT:
Also, I think Grimoire should have some improvements on it's plugin system. Relying on LoadLibrary and DllMain to start it up is just not the way of doing that. Give Grim's dlls at least 2 exported functions, say "Initialize" and "Terminate" and stop doing things on DllMain.
The way you do it today is a little bit weird. It would be good to se some interfaces or abstract classes and have those 2 functions I mentioned above. It could work like the COM "Register"/ "Unregister" mechanism.

I just don't like the idea of DllMain. It is weird, coz almost any dll can be loaded. Put some crazy dll (say MYSql dll) on grim's path, setup the confs and its will get loaded. What about the interfaces? You can't just load anything. Make those dlls have standard initialization / terminate functions, and only load those that comply with the standard. It just doesn't make any sense loading "any" dll, they should have AT LEAST some sort of interface that Grim's would provide and by using that interface, the dlls get loaded (or not).
06-11-2008, 07:06 PM#15
Ion
Ah well I totally missed out on the CreateInstance method. You're right; a static method is only used to create an instance of the plugin object but if we're using this instead it won't be necessary.

Here's the complete source for the PELoader. I simply expose the managed method load() in CLILoader to unmanaged code so it can be called by the unmanaged PELoader.
Code:
// PELoader
// Loads a .Net assembly
// Copyright (c) 2008, E.Sandberg (Risc/Ion)

#include "stdafx.h"
#include <windows.h>

typedef int (*fCall)(void);
typedef int (*LPLOAD)(void);
HINSTANCE hDLL = 0;
LPLOAD lpLoad;
EXTERN_C IMAGE_DOS_HEADER __ImageBase;
LPTSTR  strDLLPath = new TCHAR[255];

BOOL APIENTRY DllMain( HANDLE hModule, 
                       DWORD  ul_reason_for_call, 
                       LPVOID lpReserved
					 )
{
	if(ul_reason_for_call == DLL_PROCESS_ATTACH)
	{
		GetModuleFileName((HINSTANCE)&__ImageBase, strDLLPath, 255);
		int found = 0;
		int i = strlen(strDLLPath);
		while(!found)
		{
			if(strDLLPath[i] == '\\')
			{
				strDLLPath[i+1] = '\0';
				found = 1;	
			}
			i--;
		}

		hDLL = LoadLibrary(strcat(strDLLPath,"CLILoader.dll"));

		if( hDLL == NULL )
			MessageBoxA(0, strcat("Could not load ", strDLLPath), "PELoader", 0);
		else
		{
			lpLoad = (LPLOAD)GetProcAddress(hDLL, "load");
			lpLoad();
		}
	}
    return TRUE;
}

Check this out http://www.hiveworkshop.com/forums/s...ad.php?t=86274 it's a proof-of-concept string-colorizer-plugin i made for the object-editor. That version doesn't use any window-hooks so it's a kind of crude thread that searches for the right window every half second. But as I said, this is just to showcase the CLI-injection process.