Tutorial:Sims 3 New LTW

From SimsWiki
Jump to: navigation, search

Introduction

Contents

This tutorial will explain how to add a new Lifetime Wish to your game, without overriding existing game files including the core scripts, i.e. with non-core script.

The LTW that we'll make is called "Devoted Seducer". I like the variety of Lifetime Wishes (LTWs) the game provides, but since I always play the same couple (in different incarnations), I don't want LTWs that require a sim to have several partners. The "Devoted Seducer" will have to woohoo x number of times in at least 5 different places with the same sim (and if they woohoo with another sim, the count starts again).

This LTW should cover enough ground to enable you to make a wide variety of LTWs.

(I'll also mention an LTW for reaching the top of the education career, which was my first test. Both will be included in the .package file I'll provide as example.)

Prerequisites

  • You should be able to make xml tuning mods. Check out HystericalParoxysm's tutorial here: Sims 3 XML Tuning Modding
  • You should be able to make a simple scripting mod (not tied to an object) and have the environment for this set up. Check out Buzzler's tutorial for an intro on this here: Sims 3 Pure Scripting Modding
  • If you want a nice icon for your wish, you'll need to be able to make that (or have someone who'll make it for you).
  • You should be able to make everything in the Sims3GameplaySystems.dll public and have already done this for the .dll to be used in your script project. (It's really just a matter of disassembling, replacing all occurrences of " private ", " family ", and " assembly " with " public " and reassembling. For info on this you can check out BailaBaila99's Core-modding Tutorial. We will only do this to compile against! The modified Sims3GameplaySystems.dll will not be in the package of the mod.)

We'll be moving around in the following area of the code:

  • Sims3.Gameplay.DreamsAndPromises
  • Sims3.Gameplay.Objects.DreamsAndPromises (for the DreamsAndPromisesDelegateFunctions)

Creating a new node primitive

All wishes and helper wishes in The Sims 3 including the LTWs have a primitive with some basic information about them. These primitives are stored in the DreamsAndPromisesNodes.xml (S3_0333406C_00000000_9A976C90ECC75C81_DreamsAndPromisesNodes%%+_XML.xml)

Let's take a look at the one that's closest to our "Devoted Seducer" wish, the "Master Romancer" LTW:

 <Primitives>
  <Name>*WooHoo in $n Different places with $n Different Sims</Name>
  <Category>Lifetime Dreams (EP3)</Category>
  <Id>171540815</Id>
  <AcceptsNumber>True</AcceptsNumber>
  <RequiredProductVersions>EP3</RequiredProductVersions>
  <TriggerEvent>kWooHooed</TriggerEvent>
  <CheckFunction>WooHooInNPlacesWithNSimsCheckFunction</CheckFunction>
  <FeedbackFunction>WooHooInNPlacesWithNSimsFeedbackFunction</FeedbackFunction>
  <PrimaryIcon>w_lifetime_woohoo_with_x_sims</PrimaryIcon>
 </Primitives>
  • Name: This is simply a textual representation of your wish. It is not what the name will be in the UI.
  • Category: A category for the wish.
  • Id: This is the ID for the wish and equals the enum value for the DreamsAndPromises.DreamNames enum.
  • AcceptsNumber: Whether or not the wish includes a numerical part (e.g. "Upgrade $n objects" or "Play piano for $n hours")
  • RequiredProductVersions: This can be BaseGame, EP1, EP2, EP3, SP1, SP2 and needs to be one of those (i.e. you can't combine).
  • TriggerEvent: Which event will (eventually) fulfill this dream and should trigger a check. Any of the long list in Sims3.Gameplay.EventSystem.EventTypeId.
  • CheckFunction: The check function will determine whether a wish can become active, is already fulfilled, or will be trown away because they are impossible (e.g. the wish to own n properties if there aren't sufficient properties to buy to fulfill the wish, or checks of romantic wishes against family relationships).
  • FeedbackFunction: Similar to above this will update the state of the wish (e.g. count up the number of woohoos and then check if it's fulfilled).
  • PrimaryIcon: The name of the .png to use as icon for the wish.

The most important decision at this point is the enum value we want for the wish. If you look at the hex values for all wishes, they're distributed all over the place for the base game and EP1, but beginning with Ambitions and continuing with Late Night, they are grouped together. This makes me believe that they previously used an FNV32 hash (I don't know on what though; it's not the enum name) and then switched. In any case, for our purposes an FNV32 should work out fine.

I called my wish enum "major_dream_woohoo_in_num_locations_with_same_sim_velocitygrass" to look similar to other LTWs (beginning with major) and added my name to increase the probability that nobody else will use the same.

If we FNV32 to this we get 0xC9AD1094. For the primitive and the dreamtree we will need the decimal version of this so you can either go into a calculator that allows you to transform hex to dec or just use google and type in "0xC9AD1094 in decimal". You'll get the value 3383562388.

Take a note of these values. We'll need them again.

Let's see what I did for the "Devoted Seducer" LTW:

 <Primitives>
  <Name>*WooHoo $n times in 5 different locations with the same sim</Name>
  <Category>Lifetime Dreams (EP3)</Category>
  <Id>3383562388</Id>
  <AcceptsNumber>True</AcceptsNumber>
  <RequiredProductVersions>EP3</RequiredProductVersions>
  <TriggerEvent>kWooHooed</TriggerEvent>
  <CountTextFunction>WooHooInNLocationsWithSameSimCountTextFunctionVelocitygrass</CountTextFunction>
  <CheckFunction>WooHooInNLocationsWithSameSimCheckFunctionVelocitygrass</CheckFunction>
  <FeedbackFunction>WooHooInNLocationsWithSameSimFeedbackFunctionVelocitygrass</FeedbackFunction>
  <PrimaryIcon>w_lifetime_devoted_seducer_velocitygrass</PrimaryIcon>
 </Primitives>
  • Name: I chose a descriptive name for the primitive
  • Id: entered the Id we got above from FNV32 of our wish enum name
  • Category and RequiredProductVersions: left alone (I use the Woohoo in tub wish somewhere and I didn't feel the need to make sure that I'm basegame compatible. If you know your wish only uses BaseGame stuff you can change this.)
  • AcceptsNumber: left alone, because our wish also takes an input number for the number of Woohoos (the number of locations, five, will be hardcoded because I believe you can only have one input number at a time).
  • TriggerEvent: left alone, because we'll need to update the count and check after each Woohoo, so it fits.
  • CountTextFunction: Wait! Where does this come from? I wanted to provide detailed feedback for the user, so that they can see how far along they are with their wish. The CountTextFunction allows you to do that.
  • CountText/Check/FeedbackFunction: I entered my own function name, which includes my name "Velocitygrass" both to easily see that it's not a game function and for reasons that will become apparent later. This special part of the function name should be unique in both the modding community and the game code. Depending on your wish, you might not need your own functions at all. E.g. I was able to create a "Become District Superintendant" (aka Level 10 of educational career) LTW only using game functions. You can take a look at the available functions here: Sims3.Gameplay.Objects.DreamsAndPromises.DreamsAndPromisesDelegateFunctions. But you should check if they're actually implemented too. E.g. there is a function called AskOnDateCheckFunction which will always dismiss the wish as impossible.
  • PrimaryIcon: again I chose a name that fits the game's naming schemes but added my own name

For now just copy this somewhere for later use. We could simply add this to the existing DreamsAndPromisesNodes.xml, but that way, we could only ever use one mod that modifies that file and as soon as more than one person does, you'd have to pick and choose.

You also don't have to be 100% certain about all these values. You can still change them at a later point. The only thing that you should really decide at this point is the LTW enum name and as such the Id, because it will be used in the next step.

Creating a new dreamtree

The Sims 3 organizes the wishes (dreams and promises) in so-called dreamtrees. They have the type ID 0x0604ABDA (DMTR) and are exported as .dreamtree files, which contain XML for the dream trees.

Every dreamtree has a root and from that several branches and sub-branches of wishes are created. Not all of the wishes in a dreamtree are actual visible wishes. E.g. you can have a dreamtree that starts with "Have Frugal Trait", then you have an event in the sims life like "Buy some $fish". This event is also treated like wish, but it's invisible. But it can still be fulfilled and once it does the child of this wish can be triggered, which is "Learn Fishing Skill".

All of the wishes in the dream trees are called nodes, specifically "DreamNodeInstances". There is also one special node that is a "TimerNodeInstance", which as the name suggests acts as a timer that goes off after a certain amount of time. So you can say that two hours after an insane sim wakes up married there's a small chance that he or she will roll the wish to break up with their spouse.

Now what does all of this has to do with our LTW? Well, for the game to do anything at all with a wish primitive, it needs to be added to a dreamtree, because those will be loaded into the DreamsAndPromisesManager and make up the pool of possible wishes for a sim.

As an example with LTWs, let's take a look at the dreamtree for EP3 LTWs: S3_0604ABDA_00000000_0EB2F6614BC99ABD_CAS Major Dream Feeder EP3%%+DMTR.dreamtree

<?xml version="1.0" encoding="utf-8"?>
<DreamTree Version="1">
  <Category>EP3</Category>
[Lots of InstanceNodes]
  <Name>CAS Major Dream Feeder EP3</Name>
  <RequiredProductVersions>EP3</RequiredProductVersions>
  <TreeHash>1208487089</TreeHash>
</DreamTree>

The instance ID of the file in the GameplayData.package equals the FNV64 of the name. I did the same for my file, so my file will be named S3_0604ABDA_00000000_FFF3FF5699146922_CAS Major Dream Feeder Velocitygrass%%+DMTR.dreamtree (If we stick to the naming scheme from the exported files, we can simply reimport and S3PE will have all IDs and the resource name, if you choose to use it.)

The tree hash is not the FNV32, but since I didn't know what it was, I simply used FNV32 for my purposes without problems (that I'm aware of). In any case the value should be unique.

For my dreamtree it looks like this:

<?xml version="1.0" encoding="utf-8"?>
<DreamTree Version="1">
  <Category>EP3</Category>
[Some InstanceNodes]
  <Name>CAS Major Dream Feeder Velocitygrass</Name>
  <RequiredProductVersions>EP3</RequiredProductVersions>
  <TreeHash>2334124290</TreeHash>
</DreamTree>

I only changed the Name and the TreeHash (as mentioned the FNV32 of the name). The category here is not limited to game versions, it can also be something like "Traits". It doesn't seem to be used at all in the code, so it should be safe to change it. I should not that the RequiredProductVersions value does limit who can use the tree in general, though for LTWs specifically the check seems to be done on individual wish level.

The next part can be very easy or very time-consuming, depending on how thorough you want to be.

As a first test, I would suggest that you simply add two node instances, the root and your LTW. This will allow your LTW to appear in the list to choose from in CAS.

Copy the root InstanceNode from the one in CAS Major Dream Feeder EP3. You can find it by looking for "IsRoot". Only one node will have it set to true in each dreamtree.

 <InstanceNode d2p1:type="DreamNodeInstance" xmlns:d2p1="http://www.w3.org/2001/XMLSchema-instance">
  <Id>1833083376</Id>
  <ParentId>1833083376</ParentId>
  <IsRoot>true</IsRoot>
  <PrototypeId>3257365829</PrototypeId>
[More variables]
 </InstanceNode>

The PrototypeId is the Id of the primitive. So if you want to find out what type of wish any given instance node is, just search for the PrototypeId in DreamsAndPromisesNodes.xml (from above). In this case 3257365829 is a "Lifetime dream aquired" wish, which is the special wish that serves as parent wish for all LTWs.

So the PrototypeId is correct and we won't change it, nor any of the values below. What we do need to change is the Id and ParentId. Since it's the root of the dreamtree they have the same value (a root has no other node as parent node).

I do not know how these values are generated, but they have to be unique, so what I did was make up names for each node instance and FNV32 them. In my example, I took the name "CAS Major Dream Feeder Velocitygrass Root".

<!-- Lifetime aquired, FNV32 of CAS Major Dream Feeder Velocitygrass Root -->
 <InstanceNode d2p1:type="DreamNodeInstance" xmlns:d2p1="http://www.w3.org/2001/XMLSchema-instance">
  <Id>3876292066</Id>
  <ParentId>3876292066</ParentId>
  <IsRoot>true</IsRoot>
  <PrototypeId>3257365829</PrototypeId>
[More variables]
 </InstanceNode>

I also added comments before each InstanceNode, because if you have a dozen or so wishes in them, only identified by numbers, things get confusing very easily.

Okay, we're halfway there. Next up is actually adding our LTW. Let's copy the one that we base our own wish on. To find it we first look for the primitive ID of the "Master Romancer" wish in DreamsAndPromisesNodes.xml. You can either try to guess the name (not always easy, because "Master Romancer" becomes something with woohoo) or look by category (Lifetime Dreams (EP3)). Or just scroll to the beginning of the tutorial where we can see that it has the ID 171540815.

  <InstanceNode d2p1:type="DreamNodeInstance" xmlns:d2p1="http://www.w3.org/2001/XMLSchema-instance">
    <Id>3756219386</Id>
    <ParentId>1833083376</ParentId>
    <PrototypeId>171540815</PrototypeId>
    <Center>
      <X>327</X>
      <Y>-1454</Y>
    </Center>
    <FulfillmentScore>3000</FulfillmentScore>
    <TraitsZeroCheck>true</TraitsZeroCheck>
    <AgeGroups />
    <MoodFlavors />
    <Careers />
    <Skills />
    <Traits>
      <SelectedTrait>
        <Trait>CommitmentIssues</Trait>
        <Required>false</Required>
        <Excluded>false</Excluded>
      </SelectedTrait>
      <SelectedTrait>
        <Trait>Flirty</Trait>
        <Required>false</Required>
        <Excluded>false</Excluded>
      </SelectedTrait>
      <SelectedTrait>
        <Trait>GreatKisser</Trait>
        <Required>false</Required>
        <Excluded>false</Excluded>
      </SelectedTrait>
      <SelectedTrait>
        <Trait>Unflirty</Trait>
        <Required>false</Required>
        <Excluded>true</Excluded>
      </SelectedTrait>
    </Traits>
    <Relationships />
    <MinTraitsMatch>1</MinTraitsMatch>
    <InputNumber>5</InputNumber>
[Some unused Localization Key Overrides]
  </InstanceNode>

Let's take a closer look at the relevant info here:

  • Id: The unique ID of this instance node. No other instance node in any of the many, many dreamtrees must have this Id. Again, I go with the FNV32 of a made up name, in my case major_dream_woohoo_in_num_locations_with_same_sim_velocitygrass_inst.
  • ParentId: The ID of the parent node. In our case the root node is the parent node, so we'll want 3876292066 as value here.
  • PrototypeId: The ID of the primitive. In our case 3383562388, as discussed in the previous step, when we defined the primitive.
  • FulfillmentScore: The lifetime happiness points you'll get for fulfilling the wish. This is normally multiplied by 10, but Ambitious sims get a slightly higher multiplier. Since 100 Woohoos can be a pretty lengthy endeavor, I'll go with 4000, which will be 40,000 Lifetime Happiness for a non-ambitious sim.
  • TraitsZeroCheck: Actually, I'm not sure what it means (and I haven't looked into it), but my guess would be that this prevents the wish from being available before any traits have been selected for the sim.
  • Traits: These are the traits that influence if the wish will be rolled as a possible LTW for the sim. Each <SelectedTrait> includes the trait as Sims3.Gameplay.ActorSystems.TraitNames value and whether this trait should be excluded or required. If both are false, the trait merely encourages the wish. Since my LTW is all about staying with the same person for a long time, I'll exclude the trait CommitmentIssues. I'll leave Flirty and GreatKisser as encouraging traits and add HopelessRomantic and FamilyOriented to that, as well as Athletic ;) I also completely removed Unflirty. For the latest patch, these aren't as vital anymore, because you can choose the LTW from the complete list.
  • MinTraitsMatch: Based on the name I would guess the minimum number of traits that must match to make this LTW rollable.
  • InputNumber: This is the very important input number for the wish. In our case 100. If you want to mess around with other wishes (e.g. lower the number of top scores required for the very tedious "Home Design Hotshot" LTW) InputNumber is what you'll be looking for.

Okay, after making these changes, I get this:

   <InstanceNode d2p1:type="DreamNodeInstance" xmlns:d2p1="http://www.w3.org/2001/XMLSchema-instance">
    <Id>181505855</Id>
    <ParentId>3876292066</ParentId>
    <PrototypeId>3383562388</PrototypeId>
    <Center>
      <X>327</X>
      <Y>-1454</Y>
    </Center>
    <FulfillmentScore>4000</FulfillmentScore>
    <TraitsZeroCheck>true</TraitsZeroCheck>
    <AgeGroups />
    <MoodFlavors />
    <Careers />
    <Skills />
    <Traits>
      <SelectedTrait>
        <Trait>CommitmentIssues</Trait>
        <Required>false</Required>
        <Excluded>true</Excluded>
      </SelectedTrait>
      <SelectedTrait>
        <Trait>HopelessRomantic</Trait>
        <Required>false</Required>
        <Excluded>false</Excluded>
      </SelectedTrait>
      <SelectedTrait>
        <Trait>FamilyOriented</Trait>
        <Required>false</Required>
        <Excluded>false</Excluded>
      </SelectedTrait>
      <SelectedTrait>
        <Trait>Flirty</Trait>
        <Required>false</Required>
        <Excluded>false</Excluded>
      </SelectedTrait>
      <SelectedTrait>
        <Trait>GreatKisser</Trait>
        <Required>false</Required>
        <Excluded>false</Excluded>
      </SelectedTrait>
      <SelectedTrait>
        <Trait>Athletic</Trait>
        <Required>false</Required>
        <Excluded>false</Excluded>
      </SelectedTrait>
    </Traits>
    <Relationships />
    <MinTraitsMatch>1</MinTraitsMatch>
    <InputNumber>5</InputNumber>
[Some unused Localization Key Overrides]
  </InstanceNode>

If you're confused by the InputNumber "5", I've made it very low for now, because we will want to test this wish before we send a sim on the lengthy journey to fulfill it. And five is pretty doable.

The script

Now that we have a snippet of xml for the primitive and the dreamtree file, how do we get these things into the game?

The code for all the initialization is here: Sims3.Gameplay.DreamsAndPromises.DreamsAndPromisesManager.ParseDreamsAndPromises()

Well, for the dreamtree it's actually not necessary, because the game will look for all resources of type 0x604abda and parse them. Great, isn't it? Not really. The problem is that when the game does this, it won't yet know about the primitive the LTW is based on so when it comes to that InstanceNode it will choke.

However, there's an easy solution. We'll just add the dreamtree as normal xml resource and take care of parsing it ourselves. So I changed the name to S3_0333406C_00000000_FFF3FF5699146922_CAS Major Dream Feeder Velocitygrass%%+DMTR.dreamtree.

Creating the project for the .dll

At this point you'll want to create a new Microsoft Visual C# Express (VS for Visual Studio) project for your script.

My .dll will be called VelocitygrassWishes.dll and the namespace, class name and tuning variable are as follows:

[assembly: Tunable]
namespace Velocitygrass
{
    public class VelocitygrassWishes
    {
        [Tunable]
        protected static bool kVelocitygrassWishes;
    }
}

You'll want to use your own namespace and come up with your own class name and tuning variable name.

Usually it all starts with adding an EventHandler for OnWorldLoadFinishedEventHandler (at least it does for me). In this case though, we need to make sure that our stuff is loaded earlier, because the LTWs need to be available in CAS, so I chose OnStartupAppHandler. It's the first I tried and it worked well for me. At that point the general DnP stuff has been initialized, so we can add our things on top, but it's early enough to occur before you first open CAS.

        static VelocitygrassWishes()
        {
            Sims3.SimIFace.World.OnStartupAppEventHandler += new EventHandler(VelocitygrassWishes.OnStartupAppHandler);
        }

        public static void OnStartupAppHandler(object sender, EventArgs e)
        {
	    // do our initialization stuff
        }

Adding the enum

When I first thought about making new LTWs having to add enums was the biggest problem. As far as I knew enums couldn't be extended without a core mod. And a core mod would mean that I could make some LTWs for myself, but there wouldn't be any LTWs in the community to share around, because using a core mod for that purpose will make it so hard to use (not in terms of coding, but in terms of usability by others) that it might as well be impossible.

However, when I followed the trail of the of the TryParseEnum<DreamNames>, I eventually ended up in a simple <string, object> dictionary in Sims3.Gameplay.Utilities.EnumParser.mLookup. I wasn't sure if it could be as simple as adding new name-value pairs to that dictionary, but I tried it out and it worked \o/

public static void VelocitygrassAddEnums()
{
    EnumParser parser;
    Dictionary<Type, EnumParser> dictionary_ic = ParserFunctions.sCaseInsensitiveEnumParsers;
    Dictionary<Type, EnumParser> dictionary_c = ParserFunctions.sCaseSensitiveEnumParsers;
    string[] new_enum_names = { "major_dream_level_10_of_education_career",
                                "major_dream_woohoo_in_num_locations_with_same_sim_velocitygrass",
                                "be_in_steady_relationship_velocitygrass" };
    object[] new_enum_values = { 0xC6CD4F99, 0xC9AD1094, 0xD1881D16 };
    if (!dictionary_ic.TryGetValue(typeof(DreamNames), out parser))
    {
        parser = new EnumParser(typeof(DreamNames), true);
        dictionary_ic.Add(typeof(DreamNames), parser);
    }

    for (int i=0; i<new_enum_names.Length; i++)
        parser.mLookup.Add(new_enum_names[i].ToLowerInvariant(), new_enum_values[i]);

    if (!dictionary_c.TryGetValue(typeof(DreamNames), out parser))
    {
        parser = new EnumParser(typeof(DreamNames), true);
        dictionary_c.Add(typeof(DreamNames), parser);
    }
    for (int i = 0; i < new_enum_names.Length; i++)
        parser.mLookup.Add(new_enum_names[i], new_enum_values[i]);
}

This code actually adds three wish enums, one for the top of the education career (which was my first test), and also one for a minor helper dream, a check if a sim is in a steady relationship (because the existing BePartnerWithSimCheckFunction was written for teens and is only for "Partner", but not "Spouse" and "Fiancee"). You can ignore those two.

The important thing to change for you are the following lines:

    string[] new_enum_names = { "major_dream_level_10_of_education_career",
                                "major_dream_woohoo_in_num_locations_with_same_sim_velocitygrass",
                                "be_in_steady_relationship_velocitygrass" };
    object[] new_enum_values = { 0xC6CD4F99, 0xC9AD1094, 0xD1881D16 };

Here you'll use your own wish name (which we discussed earlier when creating the primitive) and the FNV32 of it, which is also the ID of the primitive:

    string[] new_enum_names = { "major_dream_woohoo_in_num_locations_with_same_sim_velocitygrass" };
    object[] new_enum_values = { 0xC6CD4F99 };

After doing this, the TryParseEnum<DreamNames> calls will correctly find your enum.

The function that adds the enums should be the first to be called in your startup handler.

Loading the primitive (part 1)

The next thing we'll do in the startup handler is to load the primitive.

To be able to do that we first need to create a brand new xml for it. I'll name mine S3_0333406C_00000000_85B44515FA88D572_VelocitygrassDreamsAndPromisesNodes%%+_XML.xml. Make sure that the resource Instance ID is the FNV64 of the name and that they are unique (adding your own name in there is always a good idea).

I'll add the <Primitives> code from above and around that the root tag from DreamsAndPromisesNodes.xml.

<?xml version="1.0"?>
<DreamsAndPromisesNodes>
[the <Primitives> from above]
</DreamsAndPromisesNodes>

Unfortunately this didn't work at all for me and after an hour or two, I finally realized that the game will read tables by always assuming the first of the children is the one with the default values.

So copy the first <Primitives> from the DreamsAndPromisesNodes.xml (with all the empty values and defaults) above your own LTW primitive and you're good to go.

To load the primitive I simply copied the static function Sims3.Gameplay.DreamsAndPromises.DreamsAndPromisesManager.ParseNodePrimitives(). I called my version ParseNodePrimitivesVelocitygrass().

Now you'll see a whole bunch of errors popping up in VS. You should resolve these by adding missing using statements and/or the class where the function/member. For everything that VS complains about go in Reflector and hover over it or click on it.

The first of these is sNodePrimitves (including the typo). If you click on it, you'll see something like this in Reflector:

Ltw tutorial reflector.png

You can find an overview of the symbols here: Class View and Object Browser Icons (The little blue dots that appear next to the fields here mean that it's a static field.)

The namespace is what you'll add to the list of using statements, in this case:

using Sims3.Gameplay.DreamsAndPromises;

Then you'll have to add the class in front of its static member so that it can be called directly.

DreamsAndPromisesManager.sNodePrimitves

Problem solved. Note that it sometimes takes a moment for VS to notice your changes. Now go on and do that with all the other things VS gives you errors about.

After that we need to make some actual changes to the code:

DreamsAndPromisesManager.sNodePrimitves = new Dictionary<uint, DreamNodePrimitive>();

This needs to be commented out, because otherwise we'd overwrite the existing primitives!

XmlDbTable table = XmlDbData.ReadData("DreamsAndPromisesNodes").Tables["Primitives"];

This needs to be changed to our own file, in my case "VelocitygrassDreamsAndPromisesNodes". (You remembered to FNV64 this name as the instance ID of the resource, right?)

Then just add the function call to ParseNodePrimitivesVelocitygrass() to OnStartupAppHandler() and we've taken care of the primitives (we'll ignore the check/count/feedback functions for now).


Loading the dreamtree

Now that we've taken care of the primitive and the game knows it, we can load our dreamtree.

Again I copied the static function, this time Sims3.Gameplay.DreamsAndPromises.DreamsAndPromisesManager.ParseDreamTrees(). I called it ParseDreamTreesVelocitygrass(). Do the same as above to get rid of all the errors VS throws at you.

Then we need to make the following changes:

DreamsAndPromisesManager.SetupDefaultNodeInstance(instanceDefults);

DreamsAndPromisesManager.sDreamTrees = new Dictionary<ulong, DreamTree>();

Comment these two lines out. We don't want to reinitialize anything, just add our stuff.

KeySearch search = new KeySearch(0x604abda);
foreach (ResourceKey key in search)

This code finds all dreamtrees and then loops through them. We only want to parse our one tree, so I replaced the code with:

ResourceKey key = new ResourceKey(0xFFF3FF5699146922, 0x333406C, 0x0);

Setting up the major dreams

If you've looked at ParseDreamsAndPromises() mentioned above, you'll have noticed SetupMajorDreams() among the initialization functions.

This is where the LTWs are processed specifically, so we need to make sure that our LTW gets this treatment too.

I copied Sims3.Gameplay.DreamsAndPromises.DreamsAndPromisesManager.SetupMajorDreams() and named it SetupMajorDreamsVelocitygrass(). Again get rid of the errors as explained above.

    DreamsAndPromisesManager.sMajorWishes = new Dictionary<uint, DreamNodeInstance>();
    DreamsAndPromisesManager.sCasFeederTrees = new List<DreamTree>();
    foreach (ulong num in kCasFeederTreeIds)
    {
        DreamTree tree;
        if (DreamsAndPromisesManager.sDreamTrees.TryGetValue(num, out tree))

The first two lines need to be commented out so that we don't overwrite existing major wishes and their trees. The foreach line can also be commented out and instead of the TryGetValue based on the num we enter our Instance ID of the dreamtree resource (which is also stored as tree ID):

if (DreamsAndPromisesManager.sDreamTrees.TryGetValue((ulong)0xFFF3FF5699146922, out tree))

Writing the check, count, and feedback functions

To get a feel for how these work, it's best to take a look at some other functions for existing wishes. They're all located in Sims3.Gameplay.Objects.DreamsAndPromises.DreamsAndPromisesDelegateFunctions.

The feedback function

Let's do the feedback function first, which does the bulk of the work in our case and allows you to keep track of the state of your wish and update it every time the trigger event happens. Then it returns whether or not the wish is now fulfilled.

How much you do in this function depends very much on the kind of LTW you'll create. If it's about something that is already stored in the game (like skill or career information or relationships), you'll only have to do the check of the values. There are even helper functions for checking maxing multiple skills, so a feedback function for let's say a "One With Nature" LTW that maxes Gardening, Fishing, and Cooking would only need the following line of code:

return MultipleSkillsAt10FeedbackHelper(node, new SkillNames[] { SkillNames.Gardening, SkillNames.Fishing, SkillNames.Cooking });

In some rare cases you might not even need to code this yourself, e.g. if you want to create an LTW for reaching the top of the Education career, because there is already a general function that checks career level (CareerLevelGreaterThanOrEqualToN).

Our case, however, will require a bit of work, which is good for purposes of the tutorial:

An ActiveDreamNode has three fields to store some additional data for the state of the wish. In my case, I've used all three of them.

  • InternalCount: This is a float that you can fill and update to keep track of a number, e.g. simoleons earned for task x or in our case the number of woohoos.
  • MiscData: This is an object and can contain just about anything. It's only used twice in the game, once to keep track of the current day for selling n baked goods in a day, and once to keep track of the current interaction of the sim for the workout for n hours straight wish. In our case, we'll use it to keep track of the sim the "Devoted Seducer" is woohooing with.
  • DataListNew: This allows you to keep track of a list of data, specifically strings or GUID or SimDescriptionId. This is used for the "Master Romancer" LTW to keep track of the objects and sims involved in the woohoos and for the visit n hotspots wish to keep track of the lot IDs. In our case we'll store the different woohoo objects (a WoohooEvent is either associated with a bed or an object).

Let's take a look at the feedback function I wrote:

[DreamsAndPromisesFeedbackFunction]
public static FeedbackResult WooHooInNLocationsWithSameSimFeedbackFunctionVelocitygrass(Event e, ActiveDreamNode node)
{
  // the code
}

The name of the function is the same one that needs to be in the primitive in the tag <FeedbackFunction>.

I started with the code from the "Master Romancer" LTW, which is in WooHooInNPlacesWithNSimsFeedbackFunction(). Like there, we check if we can get the event as a WooHooEvent and return a false feedback result if we can't.

    WooHooEvent event2 = e as WooHooEvent;
    if (event2 == null)
    {
      return new FeedbackResult(false);
    }

We'll also need to evaluate the second sim because our LTW only works with the same sim. This code can also be lifted from the "Master Romancer" feedback function:

    Sim owner = node.Owner;
    Sim sim2 = ((event2.Actor == owner) ? ((Sim)event2.TargetObject) : ((Sim)event2.Actor)) as Sim;
    if (sim2 == null)
    {
      return new FeedbackResult(false);
    }

Then we'll do a check for the last woohoo partner. This will either be a specific sim or nothing yet. If it's nothing we can initialize our fields and set the woohoo partner in MiscData. If there is already a woohoo partner set from the last time, we check if they're the same. If they are, we don't do anything, but if it's a different sim, we also reset everything. It would also be possible to keep track of ALL woohoos that the sim with the LTW does and allow him or her to switch between partners, but it would be more complicated and for my purposes it's not necessary.

    
    // reset if sim has new partner (or is cheating)
    ulong simDescriptionId = sim2.SimDescription.SimDescriptionId;
    if ((node.MiscData == null) || (((ulong)node.MiscData) != simDescriptionId))
    {
      node.InternalCount = 0f;
      node.DataListNew.Clear();
      node.MiscData = simDescriptionId;
    }

Now that we've taken care of keeping track of the woohoo partner, let's move on to the actual woohoo count, which is quite simple really:

    // count woohoo
    node.InternalCount++;

If you have a simple LTW that just counts certain things, this is all that you'll need. (Either by simply incrementing the counter as I did, or by adding information from the trigger event. Actually, if you really do nothing but increment the counter by one you can use the existing function CountFeedbackFunction().)

Next we'll keep track of the location where the woohoo took place. The code for getting the object ID is lifted from the "Master Romancer" feedback function. We don't need to do anything complicated with it however, so we'll just add it straight to the list unless it's already in there:

    // add location if new
    IGameObject obj = (event2.BedUsed != null) ? event2.BedUsed : event2.ObjectUsed;
    ulong objId = obj.ObjectId.Value;
    if (!node.DataListContains(objId))
    {
      node.AddToDataList(objId);
    }

Last but not least, there is this line:

    return new FeedbackResult(WooHooInNLocationsWithSameSimCheckFunctionVelocitygrass(node) == ActiveNodeBase.CheckResult.kMajorDreamComplete);

We could do the check for fulfillment here, but since the same thing needs to be done in the check function (to determine if the wish is available for the sim), we will just use that directly.

The check function

Since we did all the heavy lifting in the feedback function, checking if the LTW is fulfilled is easy:

[DreamsAndPromisesCheckFunction]
public static ActiveNodeBase.CheckResult WooHooInNLocationsWithSameSimCheckFunctionVelocitygrass(ActiveDreamNode node)
{
    if (node.InternalCount >= node.NodeInstance.InputNumber && node.DataListNew.Count >= 5)
        return ActiveNodeBase.CheckResult.kMajorDreamComplete;

    return ActiveNodeBase.CheckResult.kBecomeActive;
}

The check is very straightforward. We check the internal count against the input number (which is the same input number we entered for the LTW in our dreamtree) and we check if the list of distinct woohoo objects is larger or equal five. If that's the case, we return kMajorDreamComplete. In all other cases we return kBecomeActive.

The count text function

This is only necessary if you count things in your LTW and want to give feedback on the status. In simple cases you can use the general count function DefaultCountFunction, which will show your progress like this "n/x", where n will be the internal count value and x will be the input number for the wish.

In our case we'll do something a bit more fancy. As soon as the sim has woohood for the first time, we want to display a string like this: "42/100 WooHoos with Rodney in 3/5 locations".

The text itself will be set in the STBL file, but all the information must of course be provided, so our LocalizeString function call will take five additional objects as second parameter. We need the internal count and the input number of the wish to display the woohoo progress. Then we need the sim name to which the current progress is tied. And lastly we want to provide feedback for the number of locations that have already been covered by the sims.

The numbers are very easy to get. For the partner's name, we need to find the SimDescription based on the ID that we stored in MiscData. This will done give us access to the sim's first name.

We'll also want to make sure that we check if MiscData is already set. On my first test, I forgot this and suddenly had no tooltip at all for the LTW until the first WooHoo occurred.

In the end the function looks like this:

[DreamsAndPromisesCountFunction]
public static string WooHooInNLocationsWithSameSimCountTextFunctionVelocitygrass(ActiveDreamNode node)
{
    if (node.MiscData != null)
    {
        SimDescription desc = SimDescription.Find(((ulong)node.MiscData));
        return Localization.LocalizeString("Gameplay/Dreams/C9AD1094:Progress", 
                new object[] { node.InternalCount, node.NodeInstance.InputNumber, node.DataListNew.Count, 5, desc.FirstName });
    }
    else
        return Localization.LocalizeString("Gameplay/Dreams/C9AD1094:ProgressNone", new object[] { node.Owner.FirstName });
}

If you'll scroll down to the STBL step of the tutorial, you'll notice that the text strings for the LTWs look something like this: Gameplay/Dreams/C9AD1094:GenericName. I kept with this and only changed "GenericName" to "Progress" and "ProgressNone" respectively. (For the ProgressNone I pass the first name of the sim who has the wish so that we can say they haven't woohooed yet.

The hex number here is the FNV32 of your wish enum.

The order of the info that you add here doesn't matter so much, because you can decide that in the STBL.

Getting the game to use these functions

Unfortunately if you just use the name of these functions in your primitive information, the game will not be able to use them.

So we'll need to change a few minor things where we load the primitive.

Loading the primitive (part 2)

If we look at the code for handling the functions, it basically boils down to this:

Type target = Type.GetType("Sims3.Gameplay.Objects.DreamsAndPromises.DreamsAndPromisesDelegateFunctions,Sims3GameplayObjects", false);
feedbackFunction = Delegate.CreateDelegate(typeof(DreamsAndPromisesFeedbackFunctionDelegate), target, str9, false, false)
                                  as DreamsAndPromisesFeedbackFunctionDelegate;

Now we see the problem here. The CreateDelegate looks for the Function (str9 is the function name as given in the primitive in the <FeedbackFunction> tag) in the wrong place. So what we need to change is the target it's looking for. We cannot wholesale change the target to our own class and .dll because the default functions that are used as fallback will also use this, so what I did was this:

Type target_velocitygrass = Type.GetType("Velocitygrass.VelocitygrassWishes,VelocitygrassWishes", false);
feedbackFunction = Delegate.CreateDelegate(typeof(DreamsAndPromisesFeedbackFunctionDelegate), 
                       str9.Contains("Velocitygrass") ? target_velocitygrass : target, str9, false, false) 
                       as DreamsAndPromisesFeedbackFunctionDelegate;

First I created my own target with the class and .dll name of my own script. Then I slightly rewrote the CreateDelegate call so that it now assigns either target_velocitygrass or target as target depending on whether or not the name of the function contains the value "Velocitygrass".

I could have also simply replaced target with target_velocitygrass, but this way we are more flexible for the future. In my tests I had both the "Devoted Seducer" and the top of the Education career LTWs in the files. Since the education career primitive uses game functions it was necessary for me to write the code so that it covers both cases.

You can change the code for the other calls of CreateDelegate analogously and it should all work out as long as you have whatever you use in Contains("xxxx") in all your own check/feedback/count function names.

Compile the .dll

Compile the .dll so that we'll have it when we create the package.

STBL

We're almost there, I promise. What we still need to do is add the actual texts as they will appear in the tooltip for the LTW and the CAS description.

For entering translations we'll need to FNV64 the localization key. But what is that key? To find out you can use Twallan's ScriptError mod, which displays untranslated strings. In my test, the result in the CAS description looked like this:

Gameplay/Dreams/C9AD1094:GenericName
* Gameplay/Dreams/C9AD1094:FemaleName
* Gameplay/Dreams/C9AD1094:MaleName
Gameplay/Dreams/C9AD1094:Description

The second and third are the bullet points of the wish that give a quick info on the goals you need to achieve to fulfill the LTW.

The names for the keys are a bit confusing because FemaleName and MaleName has nothing to do with whether your sim is male or female. FemaleName is simply the first bullet point. MaleName is the second. Also note that the generic name is unisex, so you can't have it say "Become a mother of n babies" for a female sim and "Become a father of n babies" for a male sim. It's the same for the description.

The hex number that you see in the localization keys is the FNV32 of your wish enum (aka the primitive ID).

I believe the community provides several tools for working with STBLs. Since this is the first time I actually used localization, I simply went with what S3PE provides out of the box. If you have your own way to create STBLs, you can use that. I'll just describe here what I did.

If you haven't already, you can create a new package in S3PE now.

For the STBL, we'll add a new STBL (0x220557DA) resource. I named mine "VelocitygrassWishesTranslation_en". As instance ID I took the FNV64 of that name and replaced the first byte (the first two digits) with my language code for English (00): 0x008CE8E647E6E282

Now that you have the resource, you can go and click "Grid" and then dictionary to add things there or use the STBL editor. Both should make it possible for you to add pairs of the FNV64 of the localization key and the translation.

In my case the first pair is 0x8F13FDAC897371F0 and "Devoted Seducer" (don't add the quotes when you enter your strings). The second pair is 0x7F2E54A0B9E0816F (FNV64 of Gameplay/Dreams/C9AD1094:FemaleName) and "Woohoo {0.Number} times with the same sim in at least 5 locations". Note the {0.Number} part, which will be replaced by the input number of your LTW. The 0 is the index of the value and the Number part tells us that the value given to the localization is a number. I won't use the second bullet point, so I will leave MaleName alone. The game will just ignore it (unless you have Twallan's ScriptError mod still in). The last thing you'll add is the description stuff. I'll spare you what I wrote for the "Devoted Seducer" ;)

So much for the LTW tooltip/CAS description. But wait, haven't we forgotten something? Earlier we added the localization for the count feedback, so we need to add those texts too:

FNV64 of Gameplay/Dreams/C9AD1094:Progress becomes "{0.Number}/{1.Number} WooHoos with {4.String} in {2.Number}/{3.Number} locations". Note the order of things here. Since I added the name of the partner sim later, it has the index 4 and thus needs to come fifth in the localization call earlier. Or to put it the other way round: Since I added the sim name last in the localization call, I need to add the 4 here as index, even though the text appears third.

In our case we only used Strings and Numbers. For more ways to make the text dynamic, take a look at existing translations in STBLs that do what you want to do. (There are things like {TownName} or {0.SimFirstName} if you pass the sim or simdescription or {0.Money} for simoleon values.)

And finally 0xD22074F78A07A161 is "{0.String} hasn't woohooed with anyone yet. Get to it!" as the progress message when the sim hasn't woohooed yet.

LTW icon

Every LTW has an icon associated with it. If you're not graphically inclined or just don't care, you can simply use any of the existing .pngs. For my first test I simply kept the <PrimaryIcon> from the "Master Romancer" in my primitive.

For the final test however I created a new icon with the name: S3_2F7D0004_08000000_E68ACAAE1C351FB0_w_lifetime_devoted_seducer_velocitygrass%%+IMAG.png

It's important that the instance ID is the FNV64 of the name of the image (that you put in the <PrimaryIcon> tag in the primitive. In my case that's "w_lifetime_devoted_seducer_velocitygrass".

I have created an icon template, which is the LTW icon background that you can use as a starting point for your LTW icon.

Ltw tutorial icon template.png

Note that the size of the .png should be exactly 54x54. Otherwise it will look blurry in CAS.

Packaging it up

Yes, we're finally there!

If you haven't created a new package in the STBL step, then do it now. The following resources can be imported as files:

  • Primitive: S3_0333406C_00000000_85B44515FA88D572_VelocitygrassDreamsAndPromisesNodes%%+_XML.xml
  • Dreamtree: S3_0333406C_00000000_FFF3FF5699146922_CAS Major Dream Feeder Velocitygrass%%+DMTR.dreamtree
  • Icon: S3_2F7D0004_08000000_E68ACAAE1C351FB0_w_lifetime_devoted_seducer_velocitygrass%%+IMAG.png

If you've created your STBL file externally, you'll need to import this too.

Finally, add the .dll and the tuning file as described in the pure scripting tutorial.

Test it in game

Now we can add the package to our Mods folder and test the whole thing. With so many changes, there is a possibility that things do not work as they should. In my tests this included missing all LTWs, unresponsive GUI, and very often (whenever I messed up the dreamtree) I could move in a sim with the LTW, but when trying to actually start playing, the "Loading..." message would pop up for a second but then disappear, and I'd be stuck in overview mode of the world, having to restart.

If things go wrong, double check all the values. Did you forgot to paste a digit/letter when copy and pasting FNVs? Did you forget to comment out a line that initializes one of the DnP things? Does your feedback function maybe miss a check for null?

Related wishes

If you tested the LTW in your game, you'll probably have noticed that while the LTW itself works great and is updated and fulfilled properly, unfortunately there are no related wishes. E.g. if you create an LTW for the top of the education career, it would make sense for the first wish for the sim to roll to be "Join Education career". Also, wishes related to the LTW have these nice little special background. It tells you that fulfilling that wish will advance your LTW and also applies a multiplier of 1.5 to the fulfillment score. (In some cases, it's not exactly like that. You can take a look at Sims3.Gameplay.DreamsAndPromises.ActiveDreamNode.getAchievementPoints() for exactly how it's calculated.)

Linked wishes

The info for linked wishes is in the following file: S3_0333406C_00000000_8991481B74871C8F_LinkedWishes%%+_XML.xml

If you look at the list, it's really very simple. Most of the <Wishes> look like this:

<Wishes>
  <PrimitiveId>woohoo_in_elevator</PrimitiveId>
  <DreamId>major_dream_woohoo_in_num_different_places_with_num_different_sims</DreamId>
 </Wishes

The PrimitiveId contains the enum name of the wish. (The value of the enum is the ID of the primitive. In our example this was the FNV32; for normal game wish enum values that is not the case, but you can look up the value in Reflector. Setting the Number Format of the Reflector Options to hexadecimal might help.) The DreamId is the enum name of the LTW it is linked to. In some cases, you'll have the input subject or number of the wish in addition to these two.

Just like with the primitives, we'll want to create our own file and read this in with our script.

I called mine: S3_0333406C_00000000_48402DF14A5D26B8_VelocitygrassLinkedWishes%%+_XML.xml (Instance ID needs to be FNV64 of name)

Like for the primitives we'll want to copy the default values on top, so that the file looks like this:

<?xml version="1.0"?>
<LinkedWishes>
<Wishes>
  <PrimitiveId>0</PrimitiveId>
  <Subject></Subject>
  <Number></Number>
  <DreamId></DreamId>
 </Wishes>
[our own linked wishes]
</LinkedWishes>

For the "Devoted Seducer" I basically copied the linked wishes from the "Master Romancer" and changed the DreamId to my own. For the eduction career LTW, I copied the things from another career and changed the career to education as well as the skills that fit that career. You can add more wishes that you consider related though keep the 1.5 fulfillment bonus in mind, so you might not want to overdo it.

To read in the info we'll add another little function to our script. I copied Sims3.Gameplay.DreamsAndPromises.DreamsAndPromisesManager.ParseLinkedWishes() as ParseLinkedWishesVelocitygrass() and made the following changes (after getting rid of errors as explained above):

DreamsAndPromisesManager.sLinkedWishes = new Dictionary<uint, List<DreamsAndPromisesManager.LinkedWishInfo>>();
XmlDbTable table = XmlDbData.ReadData("LinkedWishes").Tables["Wishes"];

Comment out the first line so that we won't remove existing links and then replace "LinkedWishes" with the name of your file, in my case "VelocitygrassLinkedWishes".

If you have Reflector set to display numbers as hexadecimal, this code might cause an error in VS:

int @int = row.GetInt("Number", 0x80000000);

Just replace it with:

int @int = row.GetInt("Number", -2147483648);

Recompile the .dll and reimport it via Grid and add the new .xml file to the package and you'll see that related wishes have the special background and the multiplier applied.

Follow-up wishes (building up the dreamtree)

The linked wishes are nice and all, but how do I get the game to roll wishes for my new LTW if a sim has selected it?

This part of the LTW is the one that I spent the most time on. The way in which dreamtrees are built are simple in theory, but can get complicated if you want to fine-tune, not to mention that handling the large blocks of XML and keeping track of the IDs can be a bit problematic. I'm pretty sure EA has a nice simple drag and drop GUI for this stuff. Unfortunately we don't. After making major changes to my dreamtree, I often had to go back several times because I had one ID wrong or a block with a parent ID that I had commented out earlier, etc. But the good news is, it's doable and for simple things it's not even that hard.

I will not go into too much detail on how this works. What I did, and what I would suggest to anyone else, is to take a wish that is similar to yours and see which child nodes that LTW has in the dreamtree.

For the purposes of this tutorial, I will show you one sub-branch of the education dreamtree.

Take the dreamtree that you created in step two of this tutorial. We'll add things to that.

The first thing that follows the LTW InstanceNode is often an additional check. E.g. for Careers you will want to check if the sim is at least a young adult. (We could have also restricted this earlier in the LTW itself, I guess, but since the career I based it on (Military) didn't do that, I also didn't.)

If you look at the dreamtree for the Astronaut LTW (in S3_0604ABDA_00000000_0520CB751BD4AF93_CAS Major Dream Feeder%%+DMTR.dreamtree), you'll find that the LTW has as its only child a wish of the prototype "Is young adult or older". This is one of the invisible dreams that's not meant to displayed to the user, but rather to remain active in the background until the event/condition occurs.

  <InstanceNode d2p1:type="DreamNodeInstance" xmlns:d2p1="http://www.w3.org/2001/XMLSchema-instance">
    <Id>1973138146</Id>
    <ParentId>1172998385</ParentId>
    <PrototypeId>1797430654</PrototypeId>
    <Center>
      <X>655</X>
      <Y>-648</Y>
    </Center>
    <PromiseConnection>true</PromiseConnection>
    <DreamVisible>false</DreamVisible>
    <MaxAdvertisement>1000000000</MaxAdvertisement>
[...]
  </InstanceNode>

We can copy this InstanceNode into our own tree below our LTW. The PrototypeId will remain the same, but the ParentId needs to be set to the Id of your LTW node. For the Id I always made up a unique name that included my name somewhere and FNV32 it.

These are the steps that always need to be done: Copy the node you want to use, change the ParentId to the Id of your own node that should be parent, and then create a new Id.

The age check alone is pretty useless, but once we've determined that the sim is indeed at least a young adult, we can do what we really want to do: Make sure that the first wish they roll is the "Join Education career" wish.

For this we will want a wish of the prototype "Satisfy on Next Heart Beat". For the military career that is the one with the ID 4253919896.

This is yet another wish that isn't displayed. It's simply automatically fulfilled the next time the kDnPSynchronizeEvent ocurrs, which happens about every few seconds or so. As far as displayed wishes go, it's basically "immediately".

Now we can finally add the wish that we want displayed. As a child of the heart beat wish, you can add a wish of the prototype "Join $career". You can copy/paste the InstanceNode with 1153359033 from the military career. Make the changes of Id and ParentId as always and additionally make sure to change the InputSubject from Military to Education.

This should make sure that immediately after you start your game with the newly created sim with the LTW "Become District Superintendant", he or she will roll the wish to join the education career. If you've added the linked wish info for the "Join career" wish above, you'll even see that it has the special background for LTW related wishes (plus the bonus fulfillment score).

There are many, many things that you can finetune. You can repeat wishes after a certain time, forever or for a limited number of times. You can add additional checks.

This is a representation of the "Devoted Seducer" tree as I have it at the moment:

 1.1 Instance: 181505855 (Parent: 3876292066) is of prototype *WooHoo $n times in 5 different locations with the same sim [Devoted Seducer LTW]
  1.1.1 Instance: 2127716604 (Parent: 181505855) is of prototype Is young adult or older
   1.1.1.1 Instance: 3187298024 (Parent: 2127716604) is of prototype Be in no romantic relationship
    1.1.1.1.1 Instance: 2581908621 (Parent: 3187298024) is of prototype Satisfy on Next Heart Beat
     1.1.1.1.1.1 Instance: 923872363 (Parent: 2581908621) is of prototype Have first romance ever
      1.1.1.1.1.1.1 Instance: 3866891031 (Parent: 923872363) is of prototype WooHoo with $sim
       1.1.1.1.1.1.1.1 Instance: 1895472452 (Parent: 3866891031) is of prototype Go steady with $sim
   1.1.1.2 Instance: 3406917293 (Parent: 2127716604) is Be in steady relationship [my own wish]
    1.1.1.2.1 Instance: 2761882212 (Parent: 3406917293) is of prototype Satisfy on Next Heart Beat
     1.1.1.2.1.1 Instance: 2156618970 (Parent: 2761882212) is of prototype WooHoo with $sim
    1.1.1.2.2 Instance: 3211039683 (Parent: 3406917293) is of prototype Wakeup after at least 5 hours
     1.1.1.2.2.1 Instance: 4265656885 (Parent: 3211039683) is a Timer Node Instance
      1.1.1.2.2.1.1 Instance: 252405954 (Parent: 4265656885) is of prototype WooHoo with $sim
      1.1.1.2.2.1.2 Instance: 363084492 (Parent: 4265656885) is of prototype WooHoo in Hot Tub with $sim
      1.1.1.2.2.1.3 Instance: 2926901452 (Parent: 4265656885) is of prototype WooHoo in Elevator

It basically checks if the sim is adult and if they are, the game will start with a woohoo wish if the sim has a partner and if they're in no romantic relationship, they will roll a wish to have their first romance. For the no-romance path, they'll go from romance, to woohoo, then to going steady.

At that point the going steady path of the branch will be fulfilled. The moment it happens a WooHoo wish is rolled immediately and additionally there's the "Wakeup after at least 5 hours" wish, which is set on repeat. The timer node below that will be fulfilled immediately and then again after ten hours and the wishes that can be rolled after that are the three WooHoo wishes.

I also lowered the fulfillment scores for the WooHoos. E.g. WooHoo in Hot Tub is normally 1500, which is far too much (especially with the 1.5 multiplier) if you potentially roll it once a day.

It took me a loooooong time to get to this point and it's still not very good. The Elevator and Hot Tub wish should be tied to either having a hot tub/elevator on the lot or seeing one. A wish to woohoo twice a day might also be overkill, though if the goal is really 100 Woohoos it might not be too farfetched (and I do have Twallan's Woohooer mod). It's all a matter of balancing and adding checks and then testing it out, which is very time-consuming.


How safe will this be to share/remove?

The whole purpose of everything we did was to make it in a way that minimizes conflict with others. That's why we added our own name to basically everything. Since we didn't replace any game files and only added our own stuff with our own Ids, it should be possible for many of such wish mods to coexist peacefully.

The DnP information for the LTW and its child wishes are stored persistently in the saves. However when I tested it, I could remove the mod and load an existing save with no ill-effect. The LTW for the sim in question will simply be blank and all child wishes that were promised at that point will be gone. If the LTW wasn't fulfilled yet, a new one can be bought with the "Change Lifetime Wish" reward or you can assign it with Twallan's Master Controller. As far as I could see there were no other problems.

Some closing words

I hope this tutorial was useful for you and that we'll see some new LTWs in the future.

All information presented here is to the best of my knowledge. If you have corrections to make, whether factual, typos, formatting or anything else, don't hesitate to make the changes directly on this page.

If you're uncertain about something and would like me to clarify things, or if you believe I missed a step (and you can't edit the page to add it yourself) or have any other feedback, you can use the forum thread to ask me.

Personal tools
Namespaces

Variants
Actions
Navigation
game select
Toolbox