림월드 1.1 업데이트 내용 - solaris0115/RimWorldModGuide GitHub Wiki

원문링크

개요 1.1로 업데이트됨 모드개발 관련 요소들에 대해 다룰것.

  1. 모드 - About.xml 새로운 필드가 추가됨. packagedId: 고유 아이디값. 모드 정렬시 저 값을 기준으로 정렬하게 될것. 이제 모드 정렬할때 쓰일꺼임. 고유식별자로 만들어야함으로 다른 모드와 겹치지 않도록 자신만의 값을 지정해 넣어야함.

소개

반갑습니다. 저는 Andreas Pardeike, **Brrainz**라는 닉네임으로 활동하고 있으며 Harmony의 개발자입니다. 이 가이드는 모드 업그레이드라는 여정을 헤쳐 나가도록 돕기위한 것입니다. 저는 Ludeon Studio와 Rimworld의 개발과 연관은 없습니다. 이것은 나를 포함하는 커뮤니티의 사적인 감사의 표현입니다.

범주

이 가이드는 모더들에게 도움이 될거다, 그들의 림월드 1.1 모드에 대해. 그리고 어떻게 하모니2로 업그레이드 하는지 알려줄거다, 그리고 이유도. 또한 가이드도 제공될거다. 공통적으로 변하는 것들에 대해, 내 모드를 업그레이드 하는동안 겪은 요소들에 대해[카메라+모드]

This guide will help modders to update their C# mod to RimWorld 1.1. I will tell you how you upgrade to Harmony 2 and why. It will also serve as a guide to common changes that I encountered while I upgraded my own Camera+ mod. During the guide, I will use Visual Studio 2019. It's community edition is free. If you use a different IDE, please adapt your changes.

While I am sure that there will be changes to the xml loading part and the fields and structures of RimWorld, this guide will not cover this. My focus is C# and patching the game with Harmony. For more details, ask your favorite discord admin or visit the Ludeon forums.

New Harmony

The release of RimWorld 1.1 is in sync with the release of Harmony 2. It introduces a few new features and contains all the bug fixes and improvements that have cumulated since v1.2.0.1. I did change the syntax of the API to be more consistent which means some renaming on your side.

🔴 IMPORTANT: Harmony 1.x and 2.x are not compatible with each other. The simplest way to solve this is to not allow Harmony 1.x on RimWorld 1.1. It's a clean cut. Since you need to touch your code and your project anyway, it seems logical to update Harmony too.

This guide will help you to make the transition as smooth as possible. More on that later.

About.xml

If you don't update the About.xml file, RimWorld 1.1 will show your mod in yellow text to indicate that it is not made for the new version:

The About.xml file is shared between RimWorld 1.0 and 1.1. Currently, this results in red (but harmless) error messages in RimWorld 1.0 but works for both versions. Here is what I used:

<?xml version="1.0" encoding="utf-8"?>
<ModMetaData>
   <name>Camera+</name>
   <author>Andreas Pardeike</author>
   <supportedVersions>
      <li>1.0</li>
      <li>1.1</li>
   </supportedVersions>
   <supportedGameVersions>
      <li>1.1</li>
   </supportedGameVersions>
   <packageId>brrainz.cameraplus</packageId>
   <description>...</description>
</ModMetaData>

supportedGameVersions is the new tag for RimWorld 1.1 and I added 1.1 to the old supportedVersions tag too for good measures but I think it does not matter. The other thing new is the packageId which identifies your mod uniquely. RimWorld is pretty strict about the format so I chose a simple brrainz.cameraplus.

Folder Structure

RimWorld 1.1 mod folder builds on the previous structure that allowed for multiple versions. It is mostly the same but there are some changes that are specific for v1.1 and newer. There are several ways to configure it but I wanted to keep it simple and satisfy my OCD so I chose to keep my mod folder structure mostly the same, except for the new CameraPlus.dll and 0Harmony.dll that will be specific for RimWorld 1.1:

📦 CameraPlus
 ┃
 ┣ 📂 About					<-- One About folder only
 ┃ ┣ 📜 About.xml
 ┃ ┗ 📜 Preview.png
 ┃
 ┣ 📂 Assemblies				<-- RW1.0 assemblies (copied from old RW1.0 release)
 ┃ ┣ 📜 0Harmony.dll (1.2.0.1)
 ┃ ┗ 📜 CameraPlus.dll (for RW1.0)
 ┃
 ┣ 📂 Common					<-- I don't use the Common folder
 ┃
 ┣ 📂 Languages					<-- RW1.0
 ┣ 📂 Textures					<-- RW1.0
 ┣ 📂 other...					<-- RW1.0
 ┃
 ┣ 📂 v1.1					<-- This is the RW1.1 folder!
 ┃ ┗ 📂 Assemblies				<-- RW1.1 assemblies (compiled by mod project)
 ┃    ┣ 📜 0Harmony.dll (2.0)
 ┃    ┗ 📜 CameraPlus.dll (for RW1.1)
 ┃
 ┗📜 LoadFolders.xml				<-- Defines custom folders (I use it for 'v'1.1)

The LoadFolders.xml looks like this:

<loadFolders>
  <v1.1>
    <li>/</li>
    <li>v1.1</li>
  </v1.1>
</loadFolders>

RimWorld 1.1 per default looks for folders with the same version number (1.1) but I like my folders alphabetically relevant and chose a slightly different name: v1.1. So I use LoadFolders.xml to define a tag <v1.1/> that contains a list of folders to search for. The root <li>/</li> is required and then I add <li>v1.1</li> which points to my custom folder v1.1.

As a result, RimWorld 1.0 uses the root folders and RimWorld 1.1 finds my new Assemblies folder inside v1.1 and prefers it over the root Assemblies folder and re-uses all my different assets from before.

For more configuration possibilities, please have a look at the ModUpdating.txt document that comes with RimWorld.

Project Changes

Since RimWorld uses a new Unity version which comes with .NET 4.7.2. There are other source code changes too but since the .NET version has changed, you need to update your project. Here is how I did it, it depends on your version of Visual Studio project. You need to change your Visual Studio project from .NET 3.5 to .NET 4.7.2:

Old project format

If you are using the old project format, you can select the project in the Solution Explorer and then choose "Properties" from the right-click context menu to get to the project settings:

Change it to .NET Framework 4.7.2 and close/save the dialog.

New project format

For newer project formats, it is easiest to just edit the project file yourself. Right click on the project in the Solution Explorer and choose "Edit Project File" which opens an xml file. There you change the following then close and save the project:

<!--Old: <TargetFramework>net35</TargetFramework>-->
<TargetFramework>net472</TargetFramework>

Output path

You also want to change the output path of your mod dlls. Since I chose to have my 1.0 version fixed in the old Assemblies folder and the new version in v1.1\Assemblies I opened the project settings, clicked on Build and Configuration: All Configurations and changed the output path:

Change it to "..\v1.1\Assemblies\" and close/save the dialog.

References

Next, you will get broken references to all the Unity dlls you used in your project. RimWorld has a slightly different folder structure and the Unity API is spread over many dlls:

To fix this, remove the old references and add the new ones back. Unfortunately, this is harder than it looks because you now have 62 Unity dll's in C:\Program Files (x86)\Steam\steamapps\common\RimWorld\RimWorldWin64_Data\Managed\. You can either hand pick them or add them all to your project. Picking them by hand takes time because you need to know where your API is located:

Unity.TextMeshPro.dll                        UnityEngine.ScreenCaptureModule.dll
UnityEngine.AccessibilityModule.dll          UnityEngine.SharedInternalsModule.dll
UnityEngine.AIModule.dll                     UnityEngine.SpriteMaskModule.dll
UnityEngine.AndroidJNIModule.dll             UnityEngine.SpriteShapeModule.dll
UnityEngine.AnimationModule.dll              UnityEngine.StreamingModule.dll
UnityEngine.ARModule.dll                     UnityEngine.SubstanceModule.dll
UnityEngine.AssetBundleModule.dll            UnityEngine.TerrainModule.dll
UnityEngine.AudioModule.dll                  UnityEngine.TerrainPhysicsModule.dll
UnityEngine.ClothModule.dll                  UnityEngine.TextCoreModule.dll
UnityEngine.ClusterInputModule.dll           UnityEngine.TextRenderingModule.dll
UnityEngine.ClusterRendererModule.dll        UnityEngine.TilemapModule.dll
UnityEngine.CoreModule.dll                   UnityEngine.TLSModule.dll
UnityEngine.CrashReportingModule.dll         UnityEngine.UI.dll
UnityEngine.DirectorModule.dll               UnityEngine.UIElementsModule.dll
UnityEngine.dll                              UnityEngine.UIModule.dll
UnityEngine.DSPGraphModule.dll               UnityEngine.UmbraModule.dll
UnityEngine.FileSystemHttpModule.dll         UnityEngine.UNETModule.dll
UnityEngine.GameCenterModule.dll             UnityEngine.UnityAnalyticsModule.dll
UnityEngine.GridModule.dll                   UnityEngine.UnityConnectModule.dll
UnityEngine.HotReloadModule.dll              UnityEngine.UnityTestProtocolModule.dll
UnityEngine.ImageConversionModule.dll        UnityEngine.UnityWebRequestAssetBundleModule.dll
UnityEngine.IMGUIModule.dll                  UnityEngine.UnityWebRequestAudioModule.dll
UnityEngine.InputLegacyModule.dll            UnityEngine.UnityWebRequestModule.dll
UnityEngine.InputModule.dll                  UnityEngine.UnityWebRequestTextureModule.dll
UnityEngine.JSONSerializeModule.dll          UnityEngine.UnityWebRequestWWWModule.dll
UnityEngine.LocalizationModule.dll           UnityEngine.VehiclesModule.dll
UnityEngine.ParticleSystemModule.dll         UnityEngine.VFXModule.dll
UnityEngine.PerformanceReportingModule.dll   UnityEngine.VideoModule.dll
UnityEngine.Physics2DModule.dll              UnityEngine.VRModule.dll
UnityEngine.PhysicsModule.dll                UnityEngine.WindModule.dll
UnityEngine.ProfilerModule.dll               UnityEngine.XRModule.dll

Hand picked dll's - for the pedantic

If you only want to include the dlls you really need, you can use dnSpy to open all the Unity dll's and then search for a given API. Don't forget that Unity now has a bunch of extension methods that are more often than not defined in their own extension dll. This step took me the longest time of all because it was sort of new and cumbersome but I learned a lot of how the API is structured. It will also help you when the method you did use no longer exists in the new Unity version. The decompiler is as usually your friend.

Updating Harmomy

Most likely, you are using Harmony via NuGet. If not, I recommend doing so. To update the reference from 1.2.0.1 to 2.0 you can simply to into the NuGet Manager for the solution

and there, choose Updates [1], select the checkbox at the Lib.Harmony row and choose Update:

If you have not used NuGet yet, Simply choose Browse and search for Lib.Harmony, select it, choose your project and then the Harmony 2.0 version and install it.

Harmony 1 => 2 Changes

Note: Official Harmony 2 documentation: https://harmony.pardeike.net

A few things in Harmony were renamed to make things more consistent. The main instance is now called Harmony instead of HarmonyInstance which requires that the namespace changes from Harmony to HarmonyLib. Also, methods that work globally are now static instead of instance methods:

Most common changes

// using Harmony;
using HarmonyLib;

// var harmony = HarmonyInstance.Create("net.pardeike.test");
var harmony = new Harmony("net.pardeike.test");

// HarmonyInstance.DEBUG = true;
Harmony.DEBUG = true;
// or better use the new annotation instead:
[HarmonyDebug]

// public DynamicMethod Patch(MethodBase original, HarmonyMethod prefix = null, HarmonyMethod postfix = null, HarmonyMethod transpiler = null)
public MethodInfo Patch(MethodBase original, HarmonyMethod prefix = null, HarmonyMethod postfix = null, HarmonyMethod transpiler = null, HarmonyMethod finalizer = null) {}

// public bool HasAnyPatches(string harmonyID)
public static bool HasAnyPatches(string harmonyID) {}

// public Patches GetPatchInfo(MethodBase method)
public static Patches GetPatchInfo(MethodBase method) {}

// public Dictionary<string, Version> VersionInfo(out Version currentVersion)
public static Dictionary<string, Version> VersionInfo(out Version currentVersion) {}

// public CodeInstruction Clone(OpCode opcode, object operand)
public CodeInstruction Clone(object operand) {}

// public static IEnumerable<T> Add<T>(this IEnumerable<T> sequence, T item)
public static IEnumerable<T> AddItem<T>(this IEnumerable<T> sequence, T item) {}

New API (partial)

// new annotation on classes or methods to debug those
[HarmonyDebug]

// reverse patching attribute
[HarmonyReversePatch]

// manual patching helper
public HarmonyMethod(MethodInfo method, int priority = -1, string[] before = null, string[] after = null, bool? debug = null) {}

// reverse patching an original onto your own stub method
public static MethodInfo ReversePatch(MethodBase original, HarmonyMethod standin, MethodInfo transpiler = null) {}

// manual patching but keeping the class structure
public PatchClassProcessor ProcessorForAnnotatedClass(Type type) {}

// simple transpiler helper
public static IEnumerable<CodeInstruction> Transpilers.Manipulator(this IEnumerable<CodeInstruction> instructions, Func<CodeInstruction, bool> predicate, Action<CodeInstruction> action) {}

AccessTools

// binding flags for declared members only:
allDeclared;

// getting all types from an assembly
public static Type[] GetTypesFromAssembly(Assembly assembly) {}

// fetching declared members
public static FieldInfo DeclaredField(Type type, string name) {}
public static FieldInfo DeclaredField(Type type, int idx) {}
public static bool IsDeclaredMember<T>(this T member) where T : MemberInfo {}
public static T GetDeclaredMember<T>(this T member) where T : MemberInfo {}

// restrict search to static constructor
public static ConstructorInfo Constructor(Type type, Type[] parameters = null, bool searchForStatic = false) {}
public static List<ConstructorInfo> GetDeclaredConstructors(Type type, bool? searchForStatic = null) {}

// FieldRef can take a FieldInfo
public static FieldRef<T, F> FieldRefAccess<T, F>(FieldInfo fieldInfo) {}

// FieldRef can now be used on statics
public delegate ref F FieldRef<T, F>(T obj = default);
public delegate ref F FieldRef<F>();
public static ref F StaticFieldRefAccess<T, F>(string fieldName) {}
public static FieldRef<F> StaticFieldRefAccess<F>(FieldInfo fieldInfo) {}

// new test for nullable
public static bool IsOfNullableType<T>(T instance) {}

// create hash from multiple objects
public static int CombinedHashCode(IEnumerable<object> objects) {}

Instead of listing all changes, I rather want users to go to https://harmony.pardeike.net and read the documentation I put up. Combine that with discovering the API by using your IDE's auto-completion and you will find that not much has changed. Harmony 2 is an evolution of Harmony 1 and does not touch basics.

Regarding refactoring an existing mod, you can assume that most of the remaining API has stayed the same: annotations, helper methods from Harmony 1 and manual patching still exist. New API has been added to a lot of places: AccessTools, Traverse and other classes have better separation between members that are inherited and those that are declared. New patch types have been added: Finalizers to wrap original methods into try/catch logic and Reverse Patches to reuse all or part of original methods in your own code.

RimWorld changes

The following are changes I discovered while refactoring my mods. It's not a complete list but I hope it helps and saves time for someone:

// CellRect.GetIterator()
CellRect.GetEnumerable()

// Toils_LayDown.GroundRestEffectiveness
StatDefOf.BedRestEffectiveness.valueIfMissing

// Verse.ContentSource.LocalFolder
Verse.ContentSource.ModsFolder

// ModContentPack.Identifier
ModContentPack.PackageId

// SoundDefOf.RadioButtonClicked
SoundDefOf.Checkbox_TurnedOff;
SoundDefOf.Checkbox_TurnedOn;
SoundDefOf.Click;

// CameraDriver.GUI
CameraDriver.CameraDriverOnGUI

// RenderPawnAt has an extra bool argument
[HarmonyPatch(typeof(PawnRenderer), "RenderPawnAt")]
[HarmonyPatch(new Type[] { typeof(Vector3), typeof(RotDrawMode), typeof(bool), typeof(bool) })]

// RenderPawnInternal has an extra bool argument
[HarmonyPatch(typeof(PawnRenderer), "RenderPawnInternal")]
[HarmonyPatch(new Type[] { typeof(Vector3), typeof(float), typeof(bool), typeof(Rot4), typeof(Rot4), typeof(RotDrawMode), typeof(bool), typeof(bool), typeof(bool) })]

// Verb.TryStartCastOn now has two overloads:
public bool TryStartCastOn(LocalTargetInfo, bool, bool)
public bool TryStartCastOn(LocalTargetInfo, LocalTargetInfo, bool, bool)

// EventType.repaint
EventType.Repaint

// TerrainDef.acceptTerrainSourceFilth
TerrainDef.filthAcceptanceMask

// FilthMaker.MakeFilth
FilthMaker.TryMakeFilth

// PawnDestinationReservationManager.RegisterFaction(newFaction);
PawnDestinationReservationManager.GetPawnDestinationSetFor(newFaction);

// string Alert.GetExplanation()
TaggedString Alert.GetExplanation()

// WorkGiver_Scanner.LocalRegionsToScanFirst
WorkGiver_Scanner.MaxRegionsToScanBeforeGlobalSearch

// pawn.story.WorkTagIsDisabled(worktag)
pawn.WorkTagIsDisabled(worktag)

// HasDebugOutputAttribute
// CategoryAttribute
// ModeRestrictionPlay
DebugOutputAttribute

// SculptingSpeed
// SmeltingSpeed
// SmithingSpeed
// TailorSpeed
GeneralLaborSpeed

// PartyUtility
GatheringsUtility

// JoyGiver.TryGiveJobInPartyArea
JoyGiver.TryGiveJobInGatheringArea

// JobGiver_EatInPartyArea
JobGiver_EatInGatheringArea

// ThinkNode_ConditionalInPartyArea
ThinkNode_ConditionalInGatheringArea

// JobGiver_GetJoyInPartyArea
JobGiver_GetJoyInGatheringArea

// JobGiver_WanderInPartyArea
JobGiver_WanderInGatheringArea

// FactionDef.backstoryCategories
FactionDef.backstoryFilters

// PawnKindDef.backstoryCategories
PawnKindDef.backstoryFilters

// PoisonSpreader
Defoliator

// PoisonSpreaderShipPart
DefoliatorShipPart

// new Job
JobMaker.MakeJob()

Thank You

If you made it this far, bravo! I have a little extra for you. I have that little build script that deploys the mod from scratch every time I build the project. It deploys to two locations at the same time: Steam/RW1.1 and Standalone/RW1.0 so I can test on both at the same time. Since it does not contain any configuration you can reuse it without modification in all your mod projects.

Just read the instructions in it and copy it into your project. Enjoy and happy modding!

/Brrainz
Andreas Pardeike
🐤 @pardeike
📣 Join my Discord
🎁 https://patreon.com/pardeike


Mod install script (Windows)

REM ################ Mod build and install script (c) Andreas Pardeike 2020 ################
REM
REM Call this script from Visual Studio's Build Events post-build event command line box:
REM "$(ProjectDir)Install.bat" $(ConfigurationName) "$(ProjectDir)" "$(ProjectName)" "About Assemblies Languages Textures v1.1" "LoadFolders.xml"
REM
REM The project structure should look like this:
REM
REM Modname
REM +- .git
REM +- .vs
REM +- About
REM |  +- About.xml
REM |  +- Preview.png
REM |  +- PublishedFileId.txt
REM +- Assemblies                      <----- this is for RW1.0 + Harmony 1
REM |  +- 0Harmony.dll
REM |  +- 0Harmony.dll.mbd
REM |  +- 0Harmony.pdb
REM |  +- Modname.dll
REM |  +- Modname.dll.mbd
REM |  +- Modname.pdb
REM +- Languages
REM +- packages
REM |  +- Lib.Harmony.2.x.x
REM +- Source
REM |  +- .vs
REM |  +- obj
REM |     +- Debug
REM |     +- Release
REM |  +- Properties
REM |  +- Modname.csproj
REM |  +- Modname.csproj.user
REM |  +- packages.config
REM |  +- Install.bat                  <----- this script
REM +- Textures
REM +- v1.1
REM |  +- Assemblies                   <----- this is for RW1.1 + Harmony 2
REM |     +- 0Harmony.dll
REM |     +- 0Harmony.dll.mbd
REM |     +- 0Harmony.pdb
REM |     +- Modname.dll
REM |     +- Modname.dll.mbd
REM |     +- Modname.pdb
REM +- .gitattributes
REM +- .gitignore
REM +- LICENSE
REM +- LoadFolders.xml
REM +- README.md
REM +- Modname.sln
REM
REM Also needed are the following environment variables in the system settings (example values):
REM
REM MONO_EXE = C:\Program Files\Mono\bin\mono.exe
REM PDB2MDB_PATH = C:\Program Files\Mono\lib\mono\4.5\pdb2mdb.exe
REM RIMWORLD_DIR_STEAM = C:\Program Files (x86)\Steam\steamapps\common\RimWorld
REM RIMWORLD_DIR_STANDALONE = %USERPROFILE%\RimWorld1-0-2408Win64
REM RIMWORLD_MOD_DEBUG = --debugger-agent=transport=dt_socket,address=127.0.0.1:56000,server=y
REM
REM Finally, configure Visual Studio's Debug configuration with the rimworld exe as an external
REM program and set the working directory to the directory containing the exe.
REM
REM To debug, build the project (this script will install the mod), then run "Debug" (F5) which
REM will start RimWorld in paused state. Finally, choose "Debug -> Attach Unity Debugger" and
REM press "Input IP" and accept the default 127.0.0.1 : 56000

@ECHO ON
SETLOCAL ENABLEDELAYEDEXPANSION

SET SOLUTION_DIR=%~2
SET SOLUTION_DIR=%SOLUTION_DIR:~0,-7%
SET TARGET_DIR=%RIMWORLD_DIR_STEAM%\Mods\%~3
SET TARGET_DEBUG_DIR=%RIMWORLD_DIR_STANDALONE%\Mods\%~3
SET ZIP_EXE="C:\Program Files\7-Zip\7z.exe"

SET HARMONY_PATH=%SOLUTION_DIR%Assemblies\0Harmony.dll
SET MOD_DLL_PATH=%SOLUTION_DIR%Assemblies\%~3.dll

IF %1==Debug (
	IF EXIST "%HARMONY_PATH:~0,-4%.pdb" (
		ECHO "Creating mdb at %HARMONY_PATH%"
		"%MONO_EXE%" "%PDB2MDB_PATH%" "%HARMONY_PATH%" 1>NUL
	)
	IF EXIST "%MOD_DLL_PATH:~0,-4%.pdb" (
		ECHO "Creating mdb at %MOD_DLL_PATH%"
		"%MONO_EXE%" "%PDB2MDB_PATH%" "%MOD_DLL_PATH%" 1>NUL
	)
)

IF %1==Release (
	IF EXIST "%HARMONY_PATH%.mdb" (
		ECHO "Deleting %HARMONY_PATH%.mdb"
		DEL "%HARMONY_PATH%.mdb" 1>NUL
	)
	IF EXIST "%MOD_DLL_PATH%.mdb" (
		ECHO "Deleting %MOD_DLL_PATH%.mdb"
		DEL "%MOD_DLL_PATH%.mdb" 1>NUL
	)
)

IF EXIST "%RIMWORLD_DIR_STANDALONE%" (
	ECHO "Copying to %TARGET_DEBUG_DIR%"
	IF NOT EXIST "%TARGET_DEBUG_DIR%" MKDIR "%TARGET_DEBUG_DIR%" 1>NUL
	FOR %%D IN (%~4) DO (
		XCOPY /I /Y /E "%SOLUTION_DIR%%%D" "%TARGET_DEBUG_DIR%\%%D" 1>NUL
	)
	FOR %%D IN (%~5) DO (
		XCOPY /Y "%SOLUTION_DIR%%%D" "%TARGET_DEBUG_DIR%\*" 1>NUL
	)
)

IF EXIST "%RIMWORLD_DIR_STEAM%" (
	ECHO "Copying to %TARGET_DIR%"
	IF NOT EXIST "%TARGET_DIR%" MKDIR "%TARGET_DIR%"
	FOR %%D IN (%~4) DO (
		XCOPY /I /Y /E "%SOLUTION_DIR%%%D" "%TARGET_DIR%\%%D" 1>NUL
	)
	FOR %%D IN (%~5) DO (
		XCOPY /Y "%SOLUTION_DIR%%%D" "%TARGET_DIR%\*" 1>NUL
	)
	%ZIP_EXE% a "%TARGET_DIR%.zip" "%TARGET_DIR%" 1>NUL
)

When you run it and look at the Output Build log in Visual Studio, you can see it working:

1>------ Rebuild All started: Project: CameraPlus, Configuration: Release Any CPU ------
1>  CameraPlus -> C:\Users\andre\Source\ModRepos\CameraPlus\v1.1\Assemblies\CameraPlus.dll
1>  "Copying to C:\Users\andre\RimWorld1-0-2408Win64\Mods\CameraPlus"
1>  "Copying to C:\Program Files (x86)\Steam\steamapps\common\RimWorld\Mods\CameraPlus"
========== Rebuild All: 1 succeeded, 0 failed, 0 skipped ==========
⚠️ **GitHub.com Fallback** ⚠️