[MOD] Ironman v0.1.3

A curated forum for approved mods. Users are unable to create new topics in this forum but can reply to existing topics. Please message a moderator to have your mod moved into this forum area.
DFIronman
Posts: 40
Joined: Sat Aug 24, 2019 12:48 am

[MOD] Ironman v0.1.3

Post by DFIronman » Sun Aug 25, 2019 3:22 am

Ironman v0.1.3

IMPORTANT - PLAYERS WITH V0.1.1 OR EARLIER, UPGRADE IMMEDIATELY TO AVOID SAVE0 BEING OVERWRITTEN

Ironman at Daggerfall Unity Nexus (mirror)

- INFORMATION -

This modification allows you to optionally play Daggerfall in "Ironman" mode. At the beginning of each NEW game, you will be asked if you want to enable Ironman mode. Enabling Ironman mode will prevent you from being able to reload the game during gameplay. Additionally, ALL saves under that character's name will be deleted when you die - EVEN ONES THAT WERE CREATED BEFORE IRONMAN WAS INSTALLED. The mod should have no noticeable effect on previously-existing games.

Quicksaves in Ironman mode will be named "(Ironman) Current" and you will not be able to load any game other than "(Ironman) Current" (if it exists) or "(Ironman) Start" (if a "Current" save doesn't exist) when trying to load a game for a character with Ironman saves. This behavior can be overriden by holding down the SHIFT key. Again, when playing an Ironman game, you will not be able to load a game or Quickload a game during gameplay.

Though there should not be an issue, it's recommended that you make a backup of all of your saves before playing with this mod enabled.

- COMPATIBILITY -

No known issues at this time.

- FREQUENTLY ASKED QUESTIONS -

Q: Will this mod work with my previously-created characters?
A: No. A new character must be created to play an Ironman game.

Q: What if I want to play a non-Ironman game? Do I need to disable the mod?
A: No. When you enter the game, just select "No" when the mod asks if you want to play Ironman.

Q: Will this mod affect my other saves?
A: It shouldn't, as long as you use a fresh character name for Ironman games. Make a backup just in case.

Q: My game bugged out and I have a backup save that can correct the issue, but I can't load it. Help?
A: From the main menu, click Load Game, and then hold down the SHIFT key.

Q: I died and all of my saves except for the "(Ironman) Start" save were deleted!
A: It's a perma-death mod. Ironman characters should never use the same name as any other character.

Q: When I press my Quicksave key, I see the game has saved, but I don't see a Quicksave in my saves list?
A: For Ironman games, "Quicksave" is renamed to "(Ironman) Current"

Q: Why can't I delete my "(Ironman) Start" save?
A: All saves under the character's name must be deleted before you can delete the "(Ironman) Start" save.

Q: Why can't I save over my "(Ironman) Start" save?
A: Because it's your starting point. The start save only exists to save you time in character creation.

Q: Why can't I load my "(Ironman) Start" save?
A: You likely have an "(Ironman) Current" save already. That one must be deleted first (dying works too).

Q: Why doesn't the load button on the in-game pause menu work?
A: You cannot load games (including quickloading) when playing an Ironman game.

- Changelog -

v0.1.3 -
  • Disabled "disable pausing" for now. It caused too many issues with the game and wasn't ready for
    release yet anyway. Enabling it in the Ironman settings mod menu won't do anything.
v0.1.2 -
  • Fixed a bug where SAVE0 would be overwritten at the start of the game. Saves were not being enumerated by Daggerfall Unity that early on, causing the game to think none existed at the point of saving. Sorry!
  • Added mod setting to disable exiting the game when enemies are nearby. The process can still be killed.
  • Added mod setting to disable pausing. Gameplay will NEVER be paused (including ANY menus) when enabled. This isn't fully finished yet because 0.1.2 is a rush release due to the SAVE0 bug.
  • Slightly beautified the Ironman messages at game start.
  • Fixed an incredibly minor issue where the "Delete Save" and Save/Load buttons would display when first opening the Save Game or Load Game menus.
v0.1.1 -
  • Adjusted how saves were deleted to address a possible bug.
  • When playing an Ironman game, the game will now be saved when you attempt to exit the game. This includes by using the pause menu's exit button, Alt-F4/CMD-Q, and killing the task.
v0.1.0 -
  • First release.
Last edited by DFIronman on Wed Aug 28, 2019 10:26 am, edited 18 times in total.

DFIronman
Posts: 40
Joined: Sat Aug 24, 2019 12:48 am

Re: [MOD] Ironman

Post by DFIronman » Sun Aug 25, 2019 3:22 am

On the technical side, getting this mod to work was a chore. Initially, I just copied over the DaggerfallUnitySaveGameWindow class to a new file (IronmanSaveGameWindow) and modified it for my purposes. That worked great. In fact, I had finished this yesterday, uploaded, and even made a post here to advertise it. And then I tested it in the live version of the game, rather than playing from within the Unity editor.

And it didn't work. So I deleted the post. There was an error in the IronmanSaveGameWindow.Setup() function related to setting the Size and Position properties of the various window components due to the fact that the set accessors for BaseScreenComponent (which a majority/all of the GUI components like ListBox, Button, etc. are derived from) are marked as internal. That stopped my mod from working whatsoever in the live version of the game, though it worked fine when running it from Unity (even the .dfmod version). I guess this is because a mod is considered an external assembly, whereas anything compiled and loaded from within the Unity editor - even a mod - is not, for some reason. Always test your mods in a live install of Daggerfall Unity.

Here, I thought I was done with the mod and it turns out that it doesn't even work! So I decided to try to use Reflection to get around the "internal" property modifier. I tested it on one or two properties and it seemed to work, so I used Reflection to modify every reference to the Size and Position properties. "Great," I thought, "I'm done again!" I loaded the game up and... "Unity has stopped working." Yeah, it crashed. So I loaded it back up and tried again. And it crashed. Turns out (at least, in this specific case), even if you can successfully modify the .Size and .Position properties with Reflection, the game will crash if there's not (for some reason) at least one direct call to set a .Size or .Position property. The problem with that is an exception will be thrown because you're trying to modify an internal-marked property - like I had in the original version of this mod.

Well, that felt like running into a brick wall. So, after making a backup, I basically threw that code out and started with a new version of IronmanSaveGameWindow that derives from DaggerfallUnitySaveGameWindow. This new version makes heavy use of Reflection and intercepting windows in order to access private variables (like goButton, savesList, etc.), modify events in the parent (DaggerfallUnitySaveGameWindow) class, and even skipping a level above base.Update() (i.e. base.base.Update(), which isn't allowed) in order to retain the intended functionality of the mod. Though Reflection is considered "slow," this isn't noticeable at all in a Save/Load game menu, where all of this action takes place. For example, the call to the grandparent of IronmanSaveGameWindow ("base.base.Update()" a.k.a. DaggerfallPopupWindow.Update()), takes between 1-2 tenths of a millisecond on my low-grade machine.

Ultimately, despite the heavy use of Reflection, this new implementation is superior to the previous version as it should be much more compatible with any potential future updates of the DaggerfallUnitySaveGameWindow class (unless the updates are fairly extreme). Though there is some re-used code from DaggerfallUnitySaveGameWindow, it's not a lot and will be easier to maintain should that class undergo a serious update.

I hope this information is useful for others who may be having issues with their mods!

Edit: Also, here's an extension class I wrote (inspired by examples from various blogs/Stack Overflow posts) for ReflectionHelper that will make utilizing Reflection to get/set fields and properties much easier. In order to make my code more readable and save time, I ended up using properties with the same names as the fields I needed to access in DaggerfallUnitySaveGameWindow. Feel free to use this in your own works.

Example usage:

Code: Select all

// The "typeof(DaggerfallUnitySaveGameWindow)" is optional for all calls in the extension method.
ListBox savesList { get { return (ListBox)this.GetFieldValue("savesList", typeof(DaggerfallUnitySaveGameWindow)); } }
Button goButton { get { return (Button)this.GetFieldValue("goButton", typeof(DaggerfallUnitySaveGameWindow)); } }

// "typeof(DaggerfallUnitySaveGameWindow)" is still optional. I just include it to speed up the process
// of finding the fields/properties I need.
goButton.SetFieldValue("OnMouseClick", null, typeof(DaggerfallUnitySaveGameWindow));

// Yes, this is a Reflection call.
savesList.OnMouseDoubleClick += SaveLoadEventHandler;
ReflectionHelper extension class:

Code: Select all

    #region ReflectionHelper
    public static class ReflectionHelper
    {
        private static readonly BindingFlags _BindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;

        private static PropertyInfo GetPropertyInfo(this object obj, string property, Type startingType = null)
        {
            PropertyInfo info;

            // Use obj.GetType() if the startingType wasn't supplied or our class is not/does not descend from that Type
            Type t = ((startingType != null) && (startingType.IsAssignableFrom(obj.GetType()))) ? startingType : obj.GetType();

            startingType = t; // Just assign this in case we have to throw an exception.

            // Loop through the whole class family tree until we find it or not.
            while ((info = t.GetProperty(property, _BindingFlags)) == null && (t = t.BaseType) != null);

            if (info == null)
                throw new NullReferenceException("No property '" + property + "' in type '" + startingType + "' or its parent(s).");

            return info;
        }

        private static FieldInfo GetFieldInfo(this object obj, string field, Type startingType = null)
        {
            FieldInfo info;

            // Use obj.GetType() if the startingType wasn't supplied or our class is not/does not descend from that Type
            Type t = ((startingType != null) && (startingType.IsAssignableFrom(obj.GetType()))) ? startingType : obj.GetType();

            startingType = t; // Just assign this in case we have to throw an exception.

            // Loop through the whole class family tree until we find it or not.
            while ((info = t.GetField(field, _BindingFlags)) == null && (t = t.BaseType) != null);

            if (info == null)
                throw new NullReferenceException("No field '" + field + "' in type '" + startingType + "' or its parent class(es).");

            return info;
        }

        public static object GetPropertyByName(this object obj, string property, Type startingType = null, object[] index = null)
        {
            return GetPropertyInfo(obj, property, startingType).GetValue(obj, index);
        }

        public static object GetFieldByName(this object obj, string field, Type startingType = null)
        {
            return GetFieldInfo(obj, field, startingType).GetValue(obj);
        }

        public static void SetPropertyValue(this object obj, string property, object value, Type startingType = null, object[] index = null)
        {
            GetPropertyInfo(obj, property, startingType).SetValue(obj, value, index);
        }

        public static void SetFieldValue(this object obj, string field, object value, Type startingType = null)
        {
            GetFieldInfo(obj, field, startingType).SetValue(obj, value);
        }
    }
    #endregion
Last edited by DFIronman on Mon Aug 26, 2019 4:21 am, edited 5 times in total.

User avatar
Jay_H
Posts: 2725
Joined: Tue Aug 25, 2015 1:54 am

Re: [MOD] Ironman

Post by Jay_H » Sun Aug 25, 2019 3:45 am

YES! YES! I love this!! I've been hoping for an Ironman mode since DFU's creation! :D

DFIronman
Posts: 40
Joined: Sat Aug 24, 2019 12:48 am

Re: [MOD] Ironman

Post by DFIronman » Sun Aug 25, 2019 3:57 am

Jay_H wrote:
Sun Aug 25, 2019 3:45 am
YES! YES! I love this!! I've been hoping for an Ironman mode since DFU's creation! :D
Haha, that's awesome! Let me know if anything breaks or if you have any suggestions. I'll be watching this thread over the next few days, at least. I tested it a bunch, but as you can see from my second post, I had to refactor the code a lot and might have missed something.

Good luck!

User avatar
Jay_H
Posts: 2725
Joined: Tue Aug 25, 2015 1:54 am

Re: [MOD] Ironman

Post by Jay_H » Sun Aug 25, 2019 4:07 am

I won't be able to give it a serious try until the start of next week, so you won't hear from me until then. But this is AWEESOOMEEE!!! I'll post about anything that goes wrong.

User avatar
Jay_H
Posts: 2725
Joined: Tue Aug 25, 2015 1:54 am

Re: [MOD] Ironman

Post by Jay_H » Sun Aug 25, 2019 5:02 am

I set apart some time to try it right away, and it works so well! Buttons disappear where they should, your idea to keep a starting save for each character is excellent, and the saves disappear as they should. I killed off a new character on purpose to test it.

I'm trying the shift-Load workaround, and I can't get it to work. Is it intended to work during gameplay or on the menu only?

Augh, this is absolutely amazing. I'm doing a melee build based on Vondur's Mad Genius: Warm Ashes, slow healing, tedious travel, scanty resources... I'm traveling halfway across Daggerfall region to find better work and the sense of risk is amazing. I love this! It's a whole new game!

DFIronman
Posts: 40
Joined: Sat Aug 24, 2019 12:48 am

Re: [MOD] Ironman

Post by DFIronman » Sun Aug 25, 2019 5:33 am

Jay_H wrote:
Sun Aug 25, 2019 5:02 am
I set apart some time to try it right away, and it works so well! Buttons disappear where they should, your idea to keep a starting save for each character is excellent, and the saves disappear as they should. I killed off a new character on purpose to test it.

I'm trying the shift-Load workaround, and I can't get it to work. Is it intended to work during gameplay or on the menu only?

Augh, this is absolutely amazing. I'm doing a melee build based on Vondur's Mad Genius: Warm Ashes, slow healing, tedious travel, scanty resources... I'm traveling halfway across Daggerfall region to find better work and the sense of risk is amazing. I love this! It's a whole new game!
Haha dude, that's awesome - I'm glad you're enjoying it! The shift-loading will only work when trying to load a game from the Main Menu (or from a non-Ironman character while in-game). Also, thanks for all of your work on quests! You've done a great job so far. I use a bunch of your stuff, plus obviously, the original quest fixes and such.

User avatar
Jay_H
Posts: 2725
Joined: Tue Aug 25, 2015 1:54 am

Re: [MOD] Ironman

Post by Jay_H » Sun Aug 25, 2019 5:40 am

I noticed that it's possible to quit your game when in danger and return to your save game. What would you think about auto-saving upon game exit?

DFIronman
Posts: 40
Joined: Sat Aug 24, 2019 12:48 am

Re: [MOD] Ironman

Post by DFIronman » Sun Aug 25, 2019 5:44 am

Jay_H wrote:
Sun Aug 25, 2019 5:40 am
I noticed that it's possible to quit your game when in danger and return to your save game. What would you think about auto-saving upon game exit?
I thought about that, but someone could just kill the game process to achieve the same effect; though I might even be able to have it save in that case as well.

Are you asking because of the potential for Ironman competitions or something along the line? Excluding that, I figure anyone playing this mod probably won't want to cheat themselves of the authentic experience, because there's no real benefit to playing with it otherwise.

User avatar
Jay_H
Posts: 2725
Joined: Tue Aug 25, 2015 1:54 am

Re: [MOD] Ironman

Post by Jay_H » Sun Aug 25, 2019 5:57 am

Nah, anyone who wants to use this mod is going to play fair and follow its rules. I just mean, when you've invested so much in a character, there's such a temptation to bend the rules a little bit... And how rigid those rules are can determine whether a person bends them or not ;)

Post Reply