Tutorials:TSM Pure Scripting Modding

From SimsWiki
Revision as of 13:48, 20 May 2013 by DeeDawg (Talk | contribs)

Jump to: navigation, search

Contents

Introduction

This tutorial will guide you through crafting and compiling a scripting mod for The Sims Medieval.
If you're experienced with scripting for The Sims 3, this should feel familiar, and if you're not don't worry as this is still directed towards beginners.

We will be crafting a simple mod, that'll add an immediate interaction to every Sim which will set the game into fast-forwarding mode and display a little Hello World notification. I'll explain what an immediate interaction is later on.

Requirements

  • Microsoft Visual Studio (I'll assume you're using the project template for this tutorial)
  • Sims 3 Package Editor
  • Basic programming knowledge (not referring to the language), and understanding of the C# syntax.
  • That your game is set up to properly support mods. If you fail to accomplish that, don't even bother reading the rest of the tutorial.

While it's not required to complete or follow this tutorial, you should make a .NET assembly browser/decompiler your best friend when developing mods for TSM.


Tutorial

Getting Started

Open up Instantiator.cs in Visual Studio, and it should look like this:

using System;
using System.Collections.Generic;
using Sims3.Gameplay;
using Sims3.SimIFace;
using Sims3.UI;
using ScriptCore;

namespace DeeDawgModsFastForwarder
{
    public class Instantiator
    {
        [TunableComment("Scripting Mod Instantiator, the value doesn't matter, only its existence"), Tunable]
        protected static bool kInstantiator = false;

        /// <summary>
        /// Mod constructor.
        /// Unless it's absolutely neccessary not to, leave it unchanged.
        /// </summary>
        static Instantiator()
        {
            World.OnWorldLoadFinishedEventHandler += new EventHandler(OnWorldLoadFinishedHandler);
        }

        /// <summary>
        /// Mod initialization method.
        /// </summary>
        private static void OnWorldLoadFinishedHandler(object sender, EventArgs e)
        {
        }
    }
}

As of now, we have a little bit of code already, but it is in fact the most important part of the whole mod. First of all we have our entry point kInstantiator, which is explained in the next section. Then we have our static constructor, which in itself is technically mandatory for the type to be functional, but what's going on inside it?

Well we can't just go right ahead and do all sorts of awesome stuff, as we need to ensure that our code gets executed at the right time. Otherwise it won't be able to do anything and would probably lead to a game crash. Inside the SimIFace library, you'll find a delegate handler called OnWorldLoadFinishedEventHandler within ScriptCore.World. Now, a delegate is a reference type variable that references a method, instead of referencing an object. Like its name implies, this delegate handler call all its delegates once a world have been loaded. That would be after you've created a new kingdom or after loading an existing kingdom. This will be a good time to get our code executed.

Don't feel bad if you're confused, as it can be difficult to grasp at first. To put it in another way, OnWorldLoadFinishedEventHandler listens for the OnWorldLoadFinished event which is fired when a world have been loaded. The OnWorldLoadFinishedEventHandler then calls all methods that have been registered. Also known as callback methods. Read more about delegates at MSDN.

And then we have our callback method, which won't do anything right now, but this is where you'll initialize your mod, so to speak.

Why the Entry Point Is Important

Quote by Buzzler

C# or better the .NET framework has some concrete rules. We will "exploit" one of these rules to get our script up and running in TS3. This rule goes as follows: The first time a static field, property or method of a class gets accessed, the static constructor of that class will be called. That is to make sure that everything is in decent shape before the class has to interact with the rest of the world.

TS3 on the other hand parses all XML resources, and assigns the tunable values it finds in there to the related variables in the related classes. So. TS3 assigns a value to our static variable, the static constructor of our class will be called, and badda bing we have a foot in the door to get our code running. We will make sure TS3 actually finds an XML resource for our tunable variable later when it comes to building the package.

The same goes for TSM. The only difference is that you don't need a XML resource.

EAxian Naming Conventions

All through the game code, you'll encounter properties prefixed with a single letter, like kInstantiator. To prevent the possibility of you getting confused like I did, let me explain their meaning real quick.

  • k = the property represents a tunable value.
  • s = the property is static.
  • m = the property is private.

The First Modifications

  1. Add the following using statements:
    • Sims3.Gameplay.Actors
    • Sims3.Gameplay.Autonomy
    • Sims3.Gameplay.EventSystem
    • Sims3.Gameplay.Interactions
  2. Rename the namespace to something more sensible. Choose wisely when it comes to your namespace and pick something unique. You don't want your namespace to clash with an EAxian namespace or with another modder's namespace.

It should now look like this

using System;
using System.Collections.Generic;
using Sims3.Gameplay;
using Sims3.Gameplay.Actors;
using Sims3.Gameplay.Autonomy;
using Sims3.Gameplay.EventSystem;
using Sims3.Gameplay.Interactions;
using Sims3.SimIFace;
using Sims3.UI;
using ScriptCore;

namespace DeeDawgMods
{
    public class Instantiator
    {
        [TunableComment("Scripting Mod Instantiator, the value doesn't matter, only its existence"), Tunable]
        protected static bool kInstantiator = false;

        /// <summary>
        /// Mod constructor.
        /// Unless it's absolutely neccessary not to, leave it unchanged.
        /// </summary>
        static Instantiator()
        {
            World.OnWorldLoadFinishedEventHandler += new EventHandler(OnWorldLoadFinishedHandler);
        }

        /// <summary>
        /// Mod initialization method.
        /// </summary>
        private static void OnWorldLoadFinishedHandler(object sender, EventArgs e)
        {
        }
    }
}

As you can see, I'm using DeeDawgMods for my mods, but you MUST use something else!

The Final Code

All that remains is the rest of the code, which I've explained with comments inside the code.

using System;
using System.Collections.Generic;
using Sims3.Gameplay;
using Sims3.Gameplay.Actors;
using Sims3.Gameplay.Autonomy;
using Sims3.Gameplay.EventSystem;
using Sims3.Gameplay.Interactions;
using Sims3.SimIFace;
using Sims3.UI;
using ScriptCore;

// This is an alias, and we need it because the type 'Gameflow' is defined within two of the namespaces we use.
using GPFlow = Sims3.Gameplay.Gameflow;

namespace DeeDawgMods
{
    public class Instantiator
    {
        [TunableComment("Scripting Mod Instantiator, the value doesn't matter, only its existence"), Tunable]
        protected static bool kInstantiator = false;

        // A list of Sims, which already have the interaction added to them
        public static List<Sim> KnownSims = new List<Sim>();

        /// <summary>
        /// Mod constructor.
        /// Unless it's absolutely neccessary not to, leave it unchanged.
        /// </summary>
        static Instantiator()
        {
            World.OnWorldLoadFinishedEventHandler += new EventHandler(OnWorldLoadFinishedHandler);
        }

        /// <summary>
        /// Mod initialization method.
        /// </summary>
        private static void OnWorldLoadFinishedHandler(object sender, EventArgs e)
        {
            foreach (Sim sim in Queries.GetObjects<Sim>())
            {
                AddInteraction(sim);
            }

            // We need to add a listener for the event fired when a new Sim is instantiated,
            // and make the listener add the interaction to the newly instantiated Sim.
            EventTracker.AddListener(EventTypeId.kSimInstantiated, new ProcessEventDelegate(OnSimInstantiated));
        }

        private static ListenerAction OnSimInstantiated(Event e)
        {
            AddInteraction(e.TargetObject as Sim);

            // Making sure that the listener keep listening to the event, and not removing itself
            // after adding the interaction to just 1 Sim.
            return ListenerAction.Keep;
        }

        public static void AddInteraction(Sim sim)
        {
            // Instead of wrapping all of the code in an if-statement, we "break" early.
            // Helps keep the code clean.
            if (sim == null)
            {
                return;
            }

            // Looping through the list of Sims that have the interaction added to them already,
            // and quits if it finds the requested Sim.
            foreach (Sim knownSim in KnownSims)
            {
                if (knownSim == sim)
                {
                    return;
                }
            }

            sim.AddInteraction(FastForwarder.Singleton);
            KnownSims.Add(sim);
        }
    }

    // This is our interaction.
    // Now, as its name implies, an immediate interaction is an interaction that have an immediate
    // impact on the game. You can recognize them in-game by the little red icon before its name.
    // We define that this interaction will be between Sims and not objects or the terrain.
    public class FastForwarder : ImmediateInteraction<Sim, Sim>
    {
        // It's not necessary to create a new instance of our interaction definition every time, so we implement
        // the singleton design pattern, and make everything talk with the same instance. It's readonly
        // to prevent anything else from overwriting it.
        public static readonly InteractionDefinition Singleton = new Definition();

        // This method contains the actual interaction logic.
        protected override bool Run()
        {
            // Pretty self-explanatory.
            GPFlow.SetGameSpeed(GPFlow.GameSpeed.Double, GPFlow.SetGameSpeedContext.GameStates);

            // Our Hello World notification.
            StyledNotification.Show(new StyledNotification.Format("Hello World, from FastForwarder", StyledNotification.NotificationStyle.kSystemMessage));

            return true;
        }

        [DoesntRequireTuning]
        private sealed class Definition : ImmediateInteractionDefinition<Sim, Sim, FastForwarder>
        {
            // The name this interaction will have in-game.
            protected override string GetInteractionName(Sim actor, Sim target, InteractionObjectPair iop)
            {
                return "FF: Go to fast-forwarding mode";
            }

            // The path the player needs to go to find the interaction.
            // Like you need to go to "Friendly" to find the "Chat" interaction.
            public override string[] GetPath()
            {
                return new string[] { "DeeDawgMods" };
            }

            // This method will make sure that only the Sims that matches the criteria will have
            // the interaction shown on them. We want it shown on every Sim, so we just return true
            // without any further validation.
            protected override bool Test(Sim actor, Sim target, bool isAutonomous, ref GreyedOutTooltipCallback greyedOutTooltipCallback)
            {
                return true;
            }
        }
    }
}

This is a no-brainer, but build the assembly as release, as we don't need all the debug crap added to our assembly.

Building the Package

  1. Open S3PE.
  2. Go to File > New or CTRL+N.
  3. Go to Resource > Add... or CTRL+SHIFT+A.
  4. Choose S3SA as the type and just put a 0 as the group.
  5. Write the assembly name as the name, and click the FNV64 button.
  6. Click OK.
  7. Now mark the S3SA resource, and click the Grid button at the bottom.
  8. Import the assembly, and change the Unknown2 field to 0xE3E5B716.
  9. Click the Commit button, save the package and close S3PE.

Now move the package to your mods folder, and test it out in-game.


Credits

  • Buzzler - for which guide and information this guide is based upon.
  • Rick - for laying the ground work of TSM modding and making the .NET profile.
  • grimreefer24601 - for being awesome, and helping me out. Without his help, this tutorial wouldn't exist.
  • Peter Jones - for his tool which makes all this possible.
  • twallan - for writing the scripting mod instantiator comment, which I've stolen. :P
Personal tools
Namespaces

Variants
Actions
Navigation
game select
Toolbox