User:FallenAvatar/Code Based Mod

From wiki
< User:FallenAvatar
Revision as of 12:29, 6 February 2018 by Meduris (Talk | contribs) (changed namespace of VisualStudioAttacher to something more generic and used the namespace to reference the calls later on in AttachDebugger() (this way one should be able to use it as is without any modifications))

Jump to: navigation, search

[[Category:Modding]] Staxel also supports adding in your own code which can attach itself to interfaces within Staxel's code. This attachment is fairly rudimentary, and isn't tested as much as the other features. However, if you've ever wanted to add new features to Staxel, this is the way to do it.


Creating the Project

This page assumes that you are running Visual Studio 2017. If you run other versions of Visual Studio, locations of things may have changed. Other IDE's will definitely have different methods for some actions.

You should not need any external dependencies to develop Mods for Staxel. However, complex changes to the game may need XNA. In that case you can find what you need here: MXA

To create a new Code Based Mod:

Your New Project dialog may not look exactly like this.
  • Open Visual Studio
  • Create a New Project (File > New > Project)
  • Select a Class Library (Installed > Visual C# > Windows Classic Desktop > Class Library (.NET Framework))
    • Note You should be able to use your .Net Language of choice, but all sample code is in C#.
    • You may name it how you like, and use the newest version of the framework.
      • Tested with up to 4.7.1
  • Pick where you want your mod saved
  • Click Ok

Setting up the Project

Now the project needs to be setup to recognize Staxel's code and build itself correctly for debugging.

Add References
  • Add a Reference by Right-Clicking the Project and selecting Add > Reference
    • Click Browse at the bottom right and navigate to where Staxel is installed, and open the bin folder.
      • For Staxel on Steam on Windows, this is likely "C:\Program Files (x86)\Steam\steamapps\common\Staxel\bin"
    • Hold Ctrl (Control) and select:
      • Plukit.Base.dll
      • Staxel.dll
    • Click Add
    • Click Ok
  • Add a new Text File and name it StaxelMod.mod
    • Replace StaxelMod with what you named your project
    • Simply add "{}" without the quotation marks to the file and save it.
      • This file is a config file. However nothing is supported in it yet. In the future, this might change.
    • Right Click this file in the Solution Explorer and select properties. Change "Copy to Output Directory" to "Copy if newer"
  • Rename the Class1 class to Mod and allow Visual Studio to rename all references to it.
  • Open Mod.cs and replacing "StaxelMod" as necessary, overwrite its content with:
using Plukit.Base;
using Staxel.Items;
using Staxel.Logic;
using Staxel.Tiles;

namespace StaxelMod {
	public class Mod : Staxel.Modding.IModHookV2 {
#region Loading
		public void GameContextInitializeInit() {}
		public void ClientContextInitializeInit() {}
		public void ClientContextInitializeBefore() {}
		public void GameContextInitializeBefore() {}
		public void GameContextInitializeAfter() {}
		public void ClientContextInitializeAfter() {}
#endregion

#region Running
	#region Every Frame
		public void UniverseUpdateAfter() {}
		public void UniverseUpdateBefore( Universe universe, Timestep step ) {}
	#endregion

		public bool CanPlaceTile( Entity entity, Vector3I location, Tile tile, TileAccessFlags accessFlags ) {
			return true;
		}

		public bool CanRemoveTile( Entity entity, Vector3I location, TileAccessFlags accessFlags ) {
			return true;
		}

		public bool CanReplaceTile( Entity entity, Vector3I location, Tile tile, TileAccessFlags accessFlags ) {
			return true;
		}
#endregion

#region Closing
		public void CleanupOldSession() { }
		public void GameContextDeinitialize() { }
		public void ClientContextDeinitialize() { }
		public void Dispose() { }
#endregion

#region Reloading
		public void ClientContextReloadBefore() { }
		public void GameContextReloadBefore() { }
		public void GameContextReloadAfter() { }
		public void ClientContextReloadAfter() { }
#endregion
	}
}

Note: Currently ClientContextDeinitialize is called during initialization intead of ClientContextInitializeInit

The project is now able to be built, copied to the Staxel bin directory and loaded as a Mod, of course it doesn't do much at the moment

Setting up Debugging

This section is technically optional, but it will make your life much, much, much easier in the long run.

Build Events
  • Right click the project and select properties
  • Select the "Build Events" tab on the left.
    • Replace "C:\Program Files (x86)\Steam\steamapps\common\Staxel\" with the path for where Staxel is installed for you.
  • In the "Pre-build event command line" section add: (Taken from here and modified
echo namespace $(ProjectName) > "$(ProjectDir)\SolutionInfo.cs"
echo { >> "$(ProjectDir)\SolutionInfo.cs"
echo     ///^<summary^>Info about this binary and the solution that was used to build it.^</summary^> >> "$(ProjectDir)\SolutionInfo.cs"
echo     public static class SolutionInfo >> "$(ProjectDir)\SolutionInfo.cs"
echo     { >> "$(ProjectDir)\SolutionInfo.cs"
echo         ///^<summary^>The name of the solution^</summary^> >> "$(ProjectDir)\SolutionInfo.cs"
echo         public const string SolutionName = "$(SolutionName)"; >> "$(ProjectDir)\SolutionInfo.cs"
echo         ///^<summary^>The name of the solution file^</summary^> >> "$(ProjectDir)\SolutionInfo.cs"
echo         public const string SolutionFileName = "$(SolutionFileName)"; >> "$(ProjectDir)\SolutionInfo.cs"
echo         ///^<summary^>The name of the project^</summary^> >> "$(ProjectDir)\SolutionInfo.cs"
echo         public const string ProjectName = "$(ProjectName)"; >> "$(ProjectDir)\SolutionInfo.cs"
echo     } >> "$(ProjectDir)\SolutionInfo.cs"
echo } >> "$(ProjectDir)\SolutionInfo.cs"
  • In the "Post-build event command line" section add:
copy "$(TargetDir)$(TargetName).*" "C:\Program Files (x86)\Steam\steamapps\common\Staxel\bin\"
Debug
  • Select the "Debug" tab on the left
    • Set "Start external program" to "C:\Program Files (x86)\Steam\steamapps\common\Staxel\bin\Staxel.Client.exe"
    • Set "Working directory" to "C:\Program Files (x86)\Steam\steamapps\common\Staxel\bin\"

This is enough to get started, you will have to start a game in Staxel, switch back to Visual Studio and Debug > Attach to Process and select "Staxel.Server.NoConsole.exe" to be able to do proper debugging, but it will work

Or you can also complete the following steps

  • The following code was originally found here
  • Add a new file to your project called "VisualStudioAttacher.cs"
  • Paste the following contents into the file
/// <summary>
/// I found this gem originally on PasteBin (http://pastebin.com/pHWMP3EQ) and (http://pastebin.com/KKyBpWQW).
/// Since it was dropped there anonymously, it had little documentation, and I made a change, I thought I'd migrate it to a gist.
/// I found this snippet of code extremely useful, it flows much better than the alternative Debugger.Launch() call.
///
/// Example usage:
/// /// <summary>
/// /// Attaches a debugger, if built in with DEBUG symbol
/// /// </summary>
/// [Conditional("DEBUG")] 
/// private static void AttachDebugger()
/// {
/// 	if (!Debugger.IsAttached)
/// 	{
/// 		// do a search for any Visual Studio processes that also have our solution currently open
/// 		var vsProcess =
/// 			VisualStudioAttacher.GetVisualStudioForSolutions(
/// 				new List<string>() { "FooBar2012.sln", "FooBar.sln" });
/// 				
/// 		if (vsProcess != null) 
/// 		{
/// 			VisualStudioAttacher.AttachVisualStudioToProcess(proc, Process.GetCurrentProcess());
/// 		}
/// 		else
/// 		{
/// 			// try and attach the old fashioned way
/// 			Debugger.Launch();
/// 		}
/// 
/// 		if (Debugger.IsAttached)
/// 		{
/// 			Console.WriteLine("\t>>> Attach sucessful");
/// 		}
/// 	}
/// }
/// </summary>
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using EnvDTE;
using DTEProcess = EnvDTE.Process;
using Process = System.Diagnostics.Process;

namespace StaxelModding.Debugging {
	#region Classes

	public static class VisualStudioAttacher {
		#region Public Methods

		[DllImport( "User32" )]
		private static extern int ShowWindow( int hwnd, int nCmdShow );

		[DllImport( "ole32.dll" )]
		public static extern int CreateBindCtx( int reserved, out IBindCtx ppbc );

		[DllImport( "ole32.dll" )]
		public static extern int GetRunningObjectTable( int reserved, out IRunningObjectTable prot );


		[DllImport( "user32.dll", CharSet = CharSet.Auto, SetLastError = true )]
		public static extern bool SetForegroundWindow( IntPtr hWnd );
		[DllImport( "user32.dll", CharSet = CharSet.Auto, SetLastError = true )]
		public static extern IntPtr SetFocus( IntPtr hWnd );


		public static string GetSolutionForVisualStudio( Process visualStudioProcess ) {
			_DTE visualStudioInstance;
			if( TryGetVsInstance( visualStudioProcess.Id, out visualStudioInstance ) ) {
				try {
					return visualStudioInstance.Solution.FullName;
				} catch( Exception ) {
				}
			}
			return null;
		}

		public static Process GetAttachedVisualStudio( Process applicationProcess ) {
			IEnumerable<Process> visualStudios = GetVisualStudioProcesses();

			foreach( Process visualStudio in visualStudios ) {
				_DTE visualStudioInstance;
				if( TryGetVsInstance( visualStudio.Id, out visualStudioInstance ) ) {
					try {
						foreach( Process debuggedProcess in visualStudioInstance.Debugger.DebuggedProcesses ) {
							if( debuggedProcess.Id == applicationProcess.Id ) {
								return debuggedProcess;
							}
						}
					} catch( Exception ) {
					}
				}
			}
			return null;
		}

		public static void AttachVisualStudioToProcess( Process visualStudioProcess, Process applicationProcess ) {
			_DTE visualStudioInstance;

			if( TryGetVsInstance( visualStudioProcess.Id, out visualStudioInstance ) ) {
				//Find the process you want the VS instance to attach to...
				DTEProcess processToAttachTo = visualStudioInstance.Debugger.LocalProcesses.Cast<DTEProcess>().FirstOrDefault( process => process.ProcessID == applicationProcess.Id );

				//Attach to the process.
				if( processToAttachTo != null ) {
					processToAttachTo.Attach();

					ShowWindow( (int)visualStudioProcess.MainWindowHandle, 3 );
					SetForegroundWindow( visualStudioProcess.MainWindowHandle );
				} else {
					throw new InvalidOperationException( "Visual Studio process cannot find specified application '" + applicationProcess.Id + "'" );
				}
			}
		}

		public static Process GetVisualStudioForSolutions( List<string> solutionNames ) {
			foreach( string solution in solutionNames ) {
				Process visualStudioForSolution = GetVisualStudioForSolution( solution );
				if( visualStudioForSolution != null ) {
					return visualStudioForSolution; ;
				}
			}
			return null;
		}


		public static Process GetVisualStudioForSolution( string solutionName ) {
			IEnumerable<Process> visualStudios = GetVisualStudioProcesses();

			foreach( Process visualStudio in visualStudios ) {
				_DTE visualStudioInstance;
				if( TryGetVsInstance( visualStudio.Id, out visualStudioInstance ) ) {
					try {
						string actualSolutionName = Path.GetFileName( visualStudioInstance.Solution.FullName );

						if( string.Compare( actualSolutionName, solutionName, StringComparison.InvariantCultureIgnoreCase ) == 0 ) {
							return visualStudio;
						}
					} catch( Exception ) {
					}
				}
			}
			return null;
		}

		#endregion

		#region Private Methods

		private static IEnumerable<Process> GetVisualStudioProcesses() {
			Process[] processes = Process.GetProcesses();
			return processes.Where( o => o.ProcessName.Contains( "devenv" ) );
		}

		private static bool TryGetVsInstance( int processId, out _DTE instance ) {
			IntPtr numFetched = IntPtr.Zero;
			IRunningObjectTable runningObjectTable;
			IEnumMoniker monikerEnumerator;
			IMoniker[] monikers = new IMoniker[1];

			GetRunningObjectTable( 0, out runningObjectTable );
			runningObjectTable.EnumRunning( out monikerEnumerator );
			monikerEnumerator.Reset();

			while( monikerEnumerator.Next( 1, monikers, numFetched ) == 0 ) {
				IBindCtx ctx;
				CreateBindCtx( 0, out ctx );

				string runningObjectName;
				monikers[0].GetDisplayName( ctx, null, out runningObjectName );

				object runningObjectVal;
				runningObjectTable.GetObject( monikers[0], out runningObjectVal );

				if( runningObjectVal is _DTE && runningObjectName.StartsWith( "!VisualStudio" ) ) {
					int currentProcessId = int.Parse( runningObjectName.Split( ':' )[1] );

					if( currentProcessId == processId ) {
						instance = (_DTE)runningObjectVal;
						return true;
					}
				}
			}

			instance = null;
			return false;
		}

		#endregion
	}

	#endregion
}
  • Open Mod.cs and add a function
		[Conditional("DEBUG")]
		private void AttachDebugger() {
			// do a search for any Visual Studio processes that also have our solution currently open
			var vsProcess = StaxelModding.Debugging.VisualStudioAttacher.GetVisualStudioForSolutions( new List<string>() { SolutionInfo.SolutionFileName } );

			if( vsProcess != null ) {
				StaxelModding.Debugging.VisualStudioAttacher.AttachVisualStudioToProcess( vsProcess, Process.GetCurrentProcess() );
			} else {
				// try and attach the old fashioned way
				Debugger.Launch();
			}

			if( Debugger.IsAttached ) {
				Logger.WriteLine( "[mod] Test472 :: Attched to VisualStudio for debugging" );
			}
		}
  • Add to the function "GameContextInitializeInit"
this.AttachDebugger();
  • Add a reference to the project to "C:\Program Files (x86)\Common Files\Microsoft Shared\MSEnv\PublicAssemblies\envdte.dll"

You should now be able to simply hit F5 to run your project and have full debugging capabilities.

Interface Documentation