Results 1 to 7 of 7

Thread: The adventures of a newbie modder

  1. #1
    Hunter
    Join Date
    Dec 2017
    Posts
    120
    Rep Power
    1

    The adventures of a newbie modder

    The adventures of a newbie modder


    Warning, this is not exactly a tutorial per se, more of a journey.
    If this is not the right section, feel free to move it accordingly.

    I'm a huge fan of 7 Days, it's the first game I've played since I ditched my Dreamcast, 20 years ago and honestly, it's pretty much the only game I play.

    When A17 dropped, I was hyped AF, but the game had changed so much from previous iterations, that it just didn't click.
    So I started modding the game. But just XML modding.

    I'm a web developer and along the way, I've coded in C# for like a year.
    I kept reading about modding using C# and stuff, but this was a bit unreal for me.
    I've never decompiled a dll or anything like that and what I did on the job was far from game development.
    All of this was still in the realm of magic for me.
    But you guessed it, it was enough to pique my interest.
    I've always wanted to dive into it at some point, but you know, tomorrow.

    After reading this post https://7daystodie.com/forums/showth...d-Workstations
    I decided it was time to kick myself in the rear and start trying.

    First thing I did was to go to https://7d2dsdx.github.io/Tutorials/index.html
    I downloaded the latest release of SDX (0.7.1)
    I started reading the docs and tried to build the Cube Mod but got a System.BadImageFormatException (in Mono.Cecil.PE.ImageReader.ReadImage()).
    I assumed something somewhere didn't play well with A17. (Bummer! =/)

    However, I've read something about DMT on the forum, so I went to https://7d2dmods.github.io/HarmonyDocs/
    I downloaded the latest release and started reading the docs.
    Cool thing is, it's backward compatible with SDX so you can have everything you had with SDX plus new goodies. (Nice!)

    I encourage anyone who wants to start modding with C# to read both, they're invaluable resources.

    Phase 1:

    I started with something preexisting from the docs:

    Code:
       // Sneak Damage pop up
       [HarmonyPatch(typeof(EntityPlayerLocal))]
       [HarmonyPatch("NotifySneakDamage")]
       public class SphereII_ClearUI_NotifySneakDamage
       {
           static bool Prefix()
           {
               return false;
           }
       }
    I thought "This is cool and all, but what if I want to change the displayed text instead?"
    I needed to see the actual code, so I downloaded a free decompiler (I chose Progress by Telerik but more on that later).
    This is the original method targeted by the example script:

    Code:
        public void NotifySneakDamage(float multiplier)
        {
            this.sneakDamageText = string.Format(Localization.Get("sneakDamageBonus", string.Empty), multiplier.ToCultureInvariantString("f1")).ToUpper();
            this.sneakDamageBlendTimer.FadeIn();
        }
    So this is the first script I wrote (Fisher Price, My First Script):

    Code:
        [HarmonyPatch(typeof(EntityPlayerLocal))]
        [HarmonyPatch("NotifySneakDamage")]
        public class Sneaky_SneakDamage
        {
            static void Prefix()
            {
                this.sneakDamageText = "Sneaky!";
                this.sneakDamageBlendTimer.FadeIn();
            }
        }
    And of course, that didn't work. "This" here represents the instance of the class Sneaky_SneakDamage, not EntityPlayerLocal. (Yeah, I believe in magic.)
    Reading the docs further, I saw that Harmony could inject a reference to the instance called __instance.
    See here: https://7d2dmods.github.io/HarmonyDo...ndPostfix.html
    So next iteration:

    Code:
        [HarmonyPatch(typeof(EntityPlayerLocal))]
        [HarmonyPatch("NotifySneakDamage")]
        public class Sneaky_SneakDamage
        {
            static void Prefix(EntityPlayerLocal __instance)
            {
                __instance.sneakDamageText = "Sneaky!";
                __instance.sneakDamageBlendTimer.FadeIn();
            }
        }
    Too bad, that didn't work either, because sneakDamageText is private.
    Reading the docs further (you should read it first), I understood that you could reference a private member by prefixing it with 3 underscores.
    So here it goes:

    Code:
    using Harmony;
    using System.Reflection;
    using UnityEngine;
    using DMT;
    
    public class Sneaky
    {
        public class Sneaky_Init : IHarmony
        {
            public void Start()
            {
                Debug.Log(" Loading Patch: " + GetType().ToString());
                var harmony = HarmonyInstance.Create(GetType().ToString());
                harmony.PatchAll(Assembly.GetExecutingAssembly());
            }
        }
    
        [HarmonyPatch(typeof(EntityPlayerLocal))]
        [HarmonyPatch("NotifySneakDamage")]
        public class Sneaky_SneakDamage
        {
            static void Prefix(EntityPlayerLocal __instance, ref string ___sneakDamageText)
            {
                ___sneakDamageText = "Sneaky!";
                __instance.sneakDamageBlendTimer.FadeIn();
            }
        }
    }
    Notice the use of triple underscore before sneakDamageText and the use of ref.
    OK, this works, time for phase 2.

  2. #2
    Hunter
    Join Date
    Dec 2017
    Posts
    120
    Rep Power
    1
    Phase 2:

    Now, onto scripting for realz.

    I've started with a workbench, because, well, you've got to start somewhere.

    So, I started by copying the block workbench from blocks.xml and tried to assign it to a new class:

    Code:
    <append xpath="/blocks">
        <block name="PoweredWorkbench">
          <property name="Class" value="PoweredWorkstation"/>
          <property name="CustomIcon" value="workbench" />
          <property name="Material" value="Mmetal"/>
          <property name="MaxDamage" value="800"/>
          <property name="StabilitySupport" value="false"/>
          <property name="Shape" value="ModelEntity"/>
          <property name="Model" value="Entities/Crafting/workbenchPrefab"/>
          <property name="DisplayType" value="blockMulti" />
          <property name="MultiBlockDim" value="2,2,1"/>
          <property name="ImposterDontBlock" value="true"/>
          <property name="Place" value="TowardsPlacerInverted"/>
          <property name="OnlySimpleRotations" value="true"/>
          <property name="IsTerrainDecoration" value="true"/>
          <property name="HeatMapStrength" value="0.5"/>
          <property name="HeatMapTime" value="1200"/>
          <property name="HeatMapFrequency" value="25"/>
          <!--<property name="RecipeList" value="backpack, workbench"/>-->
          <!--<property name="CraftTimeMultiplier" value="0.5,1"/>-->
          <property name="Stacknumber" value="1"/>
    
          <property class="Workstation">
            <property name="Modules" value="output"/>
            <property name="CraftingAreaRecipes" value="player,workbench"/> <!-- Or new recipes -->
          </property>
          
          <property name="WorkstationIcon" value="ui_game_symbol_workbench" />
          <property name="OpenSound" value="open_workbench" />
          <property name="CloseSound" value="close_workbench" />
          <property name="WorkstationJournalTip" value="workbenchTip" />
    
          <property class="RepairItems">
            <property name="resourceForgedIron" value="25"/>
            <property name="resourceMechanicalParts" value="20"/>
            <property name="resourceWood" value="50"/>
          </property>
          <drop event="Harvest" name="resourceScrapIron" count="200" tag="allHarvest"/>
          <drop event="Harvest" name="resourceWood" count="20" tag="allHarvest"/>
          <drop event="Harvest" name="terrStone" count="0" tool_category="Disassemble"/>
          <drop event="Harvest" name="resourceForgedIron" count="10" tag="salvageHarvest"/>
          <drop event="Harvest" name="resourceMechanicalParts" count="8" tag="salvageHarvest"/>
          <drop event="Harvest" name="resourceWood" count="20" tag="salvageHarvest"/>
          <drop event="Destroy" count="0"/>
          <drop event="Fall" name="terrDestroyedWoodDebris" count="1" prob="0.75" stick_chance="1"/>
          <property name="TakeDelay" value="1"/>
          <property name="DescriptionKey" value="workbenchDesc"/> <!-- Would need Localization changes -->
          <property name="EconomicValue" value="776"/>
          <property name="Group" value="Building,Science"/>
          <property name="FilterTags" value="fdecor,fother,ffurniture"/>
          <property name="SortOrder1" value="70i0"/>
        </block>
      </append>
    This is the class I created to go with it:
    Code:
    using System;
    using System.Xml;
    using UnityEngine;
    using System.Globalization;
    
    public class BlockPoweredWorkstation : BlockWorkstation
    {
        public BlockPoweredWorkstation()
        {
            this.HasTileEntity = true;
        }
    }
    As you see, it's just inheriting BlockWorkstation for now.
    But when I loaded the save, I got an error message in the console saying:
    Code:
    Exception: Class 'BlockPoweredWorkstation' not found on block PoweredWorkbench!
    After reading the docs again (RTFM), I found this line interesting in https://7d2dmods.github.io/HarmonyDo...m?Scripts.html
    Code:
    <requirement name="RequirementSameFactionSDX, Mods" faction="animalsCows" />
    The name,"RequirementSameFactionSDX, Mods" is the class name along with the Assembly it's compiled into. For nearly all scripts, this assembly will be Mods, which gets created in the Mods.dll file.

    Oh...

    So I changed the class to:
    Code:
    <property name="Class" value="PoweredWorkstation, Mods"/>
    And tada, it worked.

    OK, let's try something simple for a start, let's try to change one method like GetActivationText:
    Code:
    using System;
    using System.Xml;
    using UnityEngine;
    using System.Globalization;
    
    public class BlockPoweredWorkstation : BlockWorkstation
    {
        public BlockPoweredWorkstation()
        {
            this.HasTileEntity = true;
        }
        public override string GetActivationText(WorldBase _world, BlockValue _blockValue, int _clrIdx, Vector3i _blockPos, EntityAlive _entityFocusing)
        {
            return "New activation text";
        }
    }
    Ok, this works and changes the text displayed when you look at the block.
    Now, let's do something real this time, let's change the activation commands.
    This is where the good stuff starts.
    We need 3 commands, "open", "activate" and "take":

    Code:
    public class BlockPoweredWorkstation : BlockWorkstation
    {
        private float TakeDelay = 2f;
        private BlockActivationCommand[] cmds = new BlockActivationCommand[] {
            new BlockActivationCommand("open", "campfire", false),
            new BlockActivationCommand("light", "electric_switch", false),
            new BlockActivationCommand("take", "hand", false)
        };
        public BlockPoweredWorkstation()
        {
            this.HasTileEntity = true;
        }
    
        public override BlockActivationCommand[] GetBlockActivationCommands(WorldBase _world, BlockValue _blockValue, int _clrIdx, Vector3i _blockPos, EntityAlive _entityFocusing)
        {
            TileEntityWorkstation tileEntity = (TileEntityWorkstation)_world.GetTileEntity(_clrIdx, _blockPos);
            bool isPlayerPlaced = tileEntity != null ? tileEntity.IsPlayerPlaced : false;
            bool canPlaceBlock = _world.CanPlaceBlockAt(_blockPos, _world.GetGameManager().GetPersistentLocalPlayer(), false);
            bool isMyLandProtectedBlock = _world.IsMyLandProtectedBlock(_blockPos, _world.GetGameManager().GetPersistentLocalPlayer(), false);
            
            this.cmds[0].enabled = canPlaceBlock;
            this.cmds[1].enabled = canPlaceBlock;
    
            //You can remove the station if it's inside your LCB area, placed by you and with a TakeDelay timer.
            this.cmds[2].enabled = isMyLandProtectedBlock && isPlayerPlaced && this.TakeDelay > 0f;
    
            return this.cmds;
        }
    }
    This works, but the commands don't do what they're supposed to do.
    The commands "take" and "light" seem reversed.
    (Yes, I could have just changed the order in the BlockActivationCommand[] cmds, but the switching behavior would need to be addressed at some point.)

    So:
    Code:
    ...
        public override BlockActivationCommand[] GetBlockActivationCommands(WorldBase _world, BlockValue _blockValue, int _clrIdx, Vector3i _blockPos, EntityAlive _entityFocusing)
        {
    
            TileEntityWorkstation tileEntity = (TileEntityWorkstation)_world.GetTileEntity(_clrIdx, _blockPos);
            bool isPlayerPlaced = tileEntity != null ? tileEntity.IsPlayerPlaced : false;
            bool canPlaceBlock = _world.CanPlaceBlockAt(_blockPos, _world.GetGameManager().GetPersistentLocalPlayer(), false);
            bool isMyLandProtectedBlock = _world.IsMyLandProtectedBlock(_blockPos, _world.GetGameManager().GetPersistentLocalPlayer(), false);
            
            this.cmds[0].enabled = canPlaceBlock;
            this.cmds[1].enabled = canPlaceBlock;
    
            //You can remove the station if it's inside your LCB area, placed by you and with a TakeDelay timer.
            this.cmds[2].enabled = isMyLandProtectedBlock && isPlayerPlaced && this.TakeDelay > 0f;
    
            return this.cmds;
        }
    
        public override bool OnBlockActivated(int _indexInBlockActivationCommands, WorldBase _world, int _cIdx, Vector3i _blockPos, BlockValue _blockValue, EntityAlive _player)
        {
            //First command is open / activate
            if (_indexInBlockActivationCommands == 0)
            {
                return this.OnBlockActivated(_world, _cIdx, _blockPos, _blockValue, _player);
            }
            // Second command is "Switch on"
            if (_indexInBlockActivationCommands == 1)
            {
                //This doesn't do nothing for now.
            }
            // Third command is pickup
            if (_indexInBlockActivationCommands == 2)
            {
                return this.TakeItemWithTimer(_cIdx, _blockPos, _blockValue, _player);
            }
            return false;
        }
    
        public new bool TakeItemWithTimer(int _cIdx, Vector3i _blockPos, BlockValue _blockValue, EntityAlive _player)
        {
            base.TakeItemWithTimer(_cIdx, _blockPos, _blockValue, _player);
            return true;
        }
     ...
    Tada, success!

    But... Now when I try to press E, nothing happens.
    A look in the console gives me a hint.
    WRN Window 'workstation_PoweredWorkbench' not found in XUI!

    I've taken a look at xui.xml and created a modlet like this:
    Code:
    <xui>
    	<append xpath="/xui/ruleset">	
    		<window_group name="workstation_PoweredWorkbench" controller="XUiC_WorkstationWindowGroup">
    			<window name="windowCraftingList"/>
    			<window name="craftingInfoPanel"/>
    			<window name="windowCraftingQueue"/>
    			<window name="windowOutput" />
    			<window name="windowNonPagingHeader" />
    		</window_group>
    	</append>
    </xui>
    This is a simple copy of the workbench for now.
    Alright, we now have window that opens when we press E.

    That's all for part 2.
    Last edited by Tete1805; 07-29-2019 at 05:06 PM.

  3. #3
    Hunter
    Join Date
    Dec 2017
    Posts
    120
    Rep Power
    1
    Reserved for phase 3.

  4. #4
    Hunter
    Join Date
    Dec 2017
    Posts
    120
    Rep Power
    1
    Reserved for phase 4.

  5. #5
    Hunter
    Join Date
    Dec 2017
    Posts
    120
    Rep Power
    1
    Reserved for phase 5.

  6. #6
    Scavenger PeterB's Avatar
    Join Date
    Jan 2019
    Location
    Germany
    Posts
    44
    Rep Power
    1
    Hi Tete1805, thanks for sharing.

    I was in a similar situation and have started working with Unity and C# more than a year ago. Took me a lot of effort reading through all those C# books and watching endless Unity tutorials. Thanks to Xyth tutorials I got the proper link to the game, which finally was my goal.

    My best advice to all, keep going, do not stop. Better a few hours a week than putting it aside. I have stopped at all a few months ago due to private reasons.
    Damn, now I have to recap a lot. It is a lot fun, but sometimes a tad unthankful and a very time consuming hobby. Basically a luxury, since time is precious.
    Just do not forget to play a bit on the side as well. Time goes so fast and suddenly you have not played for a few months.
    Hope this thread helps and encourage more people. Again highly appreciated and I have a lot of respect for all the modders out there.
    Cheers
    PeterB(Sundog)

  7. #7
    Hunter
    Join Date
    Dec 2017
    Posts
    120
    Rep Power
    1
    Thanks for the kind words man!
    I hope this drives more people towards modding using SDX/DMT.
    I know it's a bit to take in at first, but I hope that posts like this will slowly build up the courage some may need to jump in.
    (Like I did.)
    Honestly, it's easier than I thought to get started.
    Yes, advanced mods may be tricky, but simple stuff is pretty straight forward.

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •