Difference between revisions of "Tutorial:Sims 3 Object Modding"
HugeLunatic (Talk | contribs) (→Starting Your Code) |
(→Finding The Original Object's Class) |
||
(18 intermediate revisions by 3 users not shown) | |||
Line 5: | Line 5: | ||
| __TOC__ | | __TOC__ | ||
|} | |} | ||
− | Object | + | While the Object Mod per se isn't limited to a specific set of properties, it is always an object that uses a custom script. The most basic sort of object mod is a simple clone of the original object rigged with some new interactions. This tutorial will show you how to clone an object, write a script for it and make the object use that script. |
==What You Need== | ==What You Need== | ||
− | * '''[http://www.microsoft.com/ | + | * '''[http://www.microsoft.com/visualstudio/en-us/products/2008-editions/express Microsoft Visual C# Express 2008]''' - simply called VS later in this tutorial |
+ | :(You can try using the 2010 version, but that version seems to be buggy regarding compiling against a different mscorlib.dll.) | ||
* '''[http://dino.drealm.info/den/denforum/index.php?board=19.0 Sims3 Package Editor]''' - simply called S3PE later in this tutorial | * '''[http://dino.drealm.info/den/denforum/index.php?board=19.0 Sims3 Package Editor]''' - simply called S3PE later in this tutorial | ||
− | * '''[http:// | + | * '''[http://dino.drealm.info/den/denforum/index.php?board=20.0 Sims3 Object Cloner]''' - simply called S3OC later in this tutorial |
+ | * '''[http://www.modthesims.info/showthread.php?t=436481 .NET assembly browser/decompiler]''' - this tutorial refers to redgate .NET Reflector, simply called Reflector later on | ||
* '''A basic understanding of the C# syntax or at least any C-like language.''' | * '''A basic understanding of the C# syntax or at least any C-like language.''' | ||
− | * '''A game that is properly set up to support scripting mods. If you fail to accomplish that, you can’t hope to successfully write | + | * '''A game that is properly set up to support scripting mods. If you fail to accomplish that, you can’t hope to successfully write object mods.''' |
Line 31: | Line 33: | ||
− | ==Cloning | + | ==Cloning Your Object== |
[[Image:ObjIntClone01.jpg|right|thumb|300px]] | [[Image:ObjIntClone01.jpg|right|thumb|300px]] | ||
The next step in this process is to make your new cloned object. | The next step in this process is to make your new cloned object. | ||
− | *Open | + | *Open S3OC, Clone -> Normal Objects |
*Scroll to the object to clone, select Clone or Fix button | *Scroll to the object to clone, select Clone or Fix button | ||
− | *Enter unique username and leave all default options checked | + | *Enter a unique username and leave all default options checked |
*Change any catalog options such as name, description, price or category | *Change any catalog options such as name, description, price or category | ||
− | *Select Start, browse to | + | *Select Start, browse to your project folder and give the package a name |
<br clear="all"/> | <br clear="all"/> | ||
− | ==Finding | + | |
− | [[Image: | + | ==Finding The Original Object's Class== |
− | This next step is important because it will later be used to make your script be derived from the previous object | + | [[Image:OMT_OBJK_ScriptClass.jpg|right|thumb|300px]] |
− | *Open | + | This next step is important because it will later be used to make your script be derived from the previous object. That will ensure that the new object has all the properties of the original object. |
− | * | + | *Open the package in S3PE. |
− | * | + | *Right-click the OBJK resource and on the bottom of the popup window (context menu), click on Edit OBJK |
− | + | *Look at the content of the Data field in the Script row. That is the class name including the full namespace. (<tt>Sims3.Gameplay.Objects.Miscellaneous.StuffedAnimal</tt> in this case) | |
− | + | ||
<br clear="all"/> | <br clear="all"/> | ||
− | ==Starting Your Code== | + | ==Your Custom Script== |
− | We are going to start off by changing the namespace and the class. | + | ===Starting Your Code=== |
+ | We are going to start off by changing the namespace and the class. The namespace is like the address of a class. You can have two classes having the same name as long as they're located in different namespaces. To make sure that your classes never clash with an EAxian class or with another modder's class, you need to make your namespace unique to you. Don't use an EAxian namespace or another modder's namespace! Ever. | ||
+ | |||
+ | In case of object mods, you'll always need to put an object's class in a namespace that begins with <tt>Sims3.Gameplay.Objects</tt>. If you don't do that, the object will cause the game to crash when you try to buy it in the catalog. The example namespace in this tutorial is: | ||
<pre>namespace Sims3.Gameplay.Objects.Miscellaneous.KolipokiMod</pre> | <pre>namespace Sims3.Gameplay.Objects.Miscellaneous.KolipokiMod</pre> | ||
+ | (Remember: You need to use something different for your mod!) | ||
− | Next we | + | Next we'll change the class name. That's not absolutely necessary, but it's not good style to give a derived class the same name as its base class. It is good style however to choose all names, be it classes or fields or whatever, in a way that they indicate what they do or stand for. Now what do you think will be the better class name for our mod? <tt>SomethingIDontKnowOrWhatever</tt> or <tt>TalkingTeddy</tt>? Right. |
<pre>public class TalkingTeddy : StuffedAnimal</pre> | <pre>public class TalkingTeddy : StuffedAnimal</pre> | ||
− | Now | + | Now what is that "StuffedAnimal" stuff after the colon for? The colon basically means "derived from". It can also mean "implements" in case of interfaces, but we'll try to reduce the confusion to a minimum here. In this case <tt>TalkingTeddy</tt> is derived from <tt>StuffedAnimal</tt>. That is a core feature of object oriented programming and means that TalkingTeddy does not only share all properties of a StuffedAnimal, it IS in fact a StuffedAnimal while a StuffedAnimal isn't necessarily a TalkingTeddy. Think of how a weiner dog is a special type of dog, but a dog isn't necessarily a weiner dog and you'll get the idea. |
− | + | For more advanced object mods, you'll derive your class from the abstract <tt>GameObject</tt> class which is the base class for all objects visible in the game, sims included. <tt>GameObject</tt> has no specific properties, so it's for the best to stick with expanding existing objects for the beginning. | |
− | + | ||
− | + | The base class should be what was in the script class entry of your object. VS will show known classes in light blue, but <tt>StuffedAnimal</tt> part of your class declaraion will be shown in black. Maybe it even gives you an error like this: | |
+ | <pre>Error 1 The type or namespace name 'StuffedAnimal' could not be found (are you missing a using directive or an assembly reference?)</pre> | ||
− | + | At the top of your code, add <tt>using Sims3.Gameplay.Objects.Miscellaneous;</tt>. You'll know where. <tt>StuffedAnimal</tt> will now be shown in light blue, and if you got the stated error, it should vanish if you press F6. The <tt>using</tt> directive basically tells VS an address and to assume that everything you write might refer to that address. So if we write <tt>StuffedAnimal</tt> now, VS will assume that we in fact mean <tt>Sims3.Gameplay.Objects.Miscellaneous.StuffedAnimal</tt>, and obviously that's exactly what we mean. That works well as long as everything we write is unambiguous. If there were two <tt>StuffedAnimal</tt> classes and we used two <tt>using</tt> directives with the addresses to these classes, then VS obviously couldn't tell which one we mean when we just write <tt>StuffedAnimal</tt>. In that case we'd need to in fact use <tt>Sims3.Gameplay.Objects.Miscellaneous.StuffedAnimal</tt> to specifiy what exactly we mean. | |
− | + | ||
− | + | ||
− | + | While we're at it, add the following <tt>using</tt> directives as well, so VS knows what we mean later on. | |
+ | <pre> | ||
+ | using Sims3.Gameplay.Interactions; | ||
+ | using Sims3.Gameplay.Actors; | ||
+ | using Sims3.Gameplay.Autonomy; | ||
+ | using Sims3.SimIFace; | ||
+ | using Sims3.UI; | ||
+ | </pre> | ||
+ | |||
+ | ===What's An Interaction?=== | ||
+ | |||
+ | Through this step we will be using Reflector so make sure you have it opened and that you have opened the libraries that we extracted in the first step. Let's have a look at how interactions are defined: | ||
+ | |||
+ | |||
+ | *Expand Sims3GameplayObjects -> Sims3GameplayObjects.dll -> Sims3.Gameplay.Objects.HobbiesSkills -> Easel -> ScrapPaintingInteraction | ||
+ | *Open the dissasembler by hitting the spacebar. | ||
+ | *Click on "Expand Methods" on the bottom of the Disassembler window. | ||
− | |||
− | + | The <tt>ScrapPaintingInteraction</tt> interaction is a very simple interaction. Let's look at it in more detail: | |
<pre> | <pre> | ||
− | + | public sealed class ScrapPaintingInteraction : ImmediateInteraction<Sim, Easel> | |
{ | { | ||
− | |||
public static readonly InteractionDefinition Singleton = new Definition(); | public static readonly InteractionDefinition Singleton = new Definition(); | ||
− | |||
− | + | public override bool Run() | |
− | + | ||
{ | { | ||
− | + | if (base.Target.CurrentCanvas.Artist == base.Actor.SimDescription) | |
+ | { | ||
+ | EventTracker.SendEvent(EventTypeId.kScrappedPainting, base.Actor); | ||
+ | } | ||
+ | base.Target.ScrapPainting(); | ||
+ | return true; | ||
} | } | ||
+ | |||
+ | [DoesntRequireTuning] | ||
+ | private sealed class Definition : ImmediateInteractionDefinition<Sim, Easel, Easel.ScrapPaintingInteraction> | ||
+ | { | ||
+ | public override string GetInteractionName(Sim a, Easel target, InteractionObjectPair interaction) | ||
+ | { | ||
+ | return Easel.LocalizeString("ScrapPainting", new object[0x0]); | ||
+ | } | ||
+ | |||
+ | public override bool Test(Sim a, Easel target, bool isAutonomous, ref GreyedOutTooltipCallback greyedOutTooltipCallback) | ||
+ | { | ||
+ | return (target.CanScrap(a) && !target.InUse); | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | </pre> | ||
+ | |||
+ | |||
+ | Let's look at a few key parts: | ||
+ | |||
+ | |||
+ | =====Interaction Declaration===== | ||
+ | <pre>public sealed class ScrapPaintingInteraction : ImmediateInteraction<Sim, Easel></pre> | ||
+ | |||
+ | * The interaction is declared as <tt>public sealed</tt>. Public basically means that all other code can "see" the class; sealed means that no sub classes may be derived from it. | ||
+ | |||
+ | * The class is derived from <tt>ImmediateInteraction</tt>. ImmediateInteractions are interactions that don't get added to a sim's interaction queue and run immediately no matter and not disturbing what a sim is doing. The <tt><Sim, Easel></tt> declares that the <tt>Actor</tt> of the interaction is a sim and the <tt>Target</tt> is an easel. Also a ImmediateInteraction can usually be recognized by the orange circle on the button in the pie menu. | ||
+ | |||
+ | |||
+ | =====Running===== | ||
+ | <pre>public override bool Run()</pre> | ||
+ | |||
+ | * The <tt>Run()</tt> method is where the actual action happens. We'll come back to that later. | ||
+ | |||
+ | |||
+ | =====Definition===== | ||
+ | <pre> | ||
+ | [DoesntRequireTuning] | ||
+ | private sealed class Definition : ImmediateInteractionDefinition<Sim, Easel, Easel.ScrapPaintingInteraction> | ||
+ | </pre> | ||
+ | |||
+ | * In the game, an actual interaction is something with an actual actor and an actual target, in this case one specific sim and one specific easel. The abstract idea of an interaction that might or might not run at some point in the future, including a prospective sim and a prospective easel, is referenced by an InteractionDefinition. | ||
+ | |||
+ | * In this case <tt>Definition</tt> is derived from <tt>ImmediateInteractionDefinition</tt> which is appropriate for an ImmediateInteraction. <tt><Sim, Easel, Easel.ScrapPaintingInteraction></tt> isn't much different from the declaration of the Interaction, except that the last part declares it as an InteractionDefinition for a <tt>Easel.ScrapPaintingInteraction</tt>. | ||
+ | |||
+ | * The <tt>[DoesntRequireTuning]</tt> attribute tells the game that there's no ITUN resource containing tuning for this interaction, so it doesn't need to bother looking for one. Adding tuning to your interactions goes beyond the scope of this tutorial; just use the attribute whenever you write an interaction without tuning. | ||
+ | |||
+ | |||
+ | =====Singleton===== | ||
+ | <pre>public static readonly InteractionDefinition Singleton = new Definition();</pre> | ||
+ | |||
+ | * The static Singleton field is that mentioned InteractionDefinition the game uses to reference the mentioned "idea of an interaction". We'll see later how to use that. | ||
+ | |||
+ | |||
+ | =====Getting A Name===== | ||
+ | <pre>protected override string GetInteractionName(Sim a, Easel target, InteractionObjectPair interaction)</pre> | ||
+ | |||
+ | * The <tt>GetInteractionName()</tt> method gets used for the string on the interaction button in the pie menu and the mouse over text on the interaction's thumbnail in a sim's interaction queue. Of course an ImmediateInteraction doesn't show up in the queue so the latter part is JFYI. | ||
+ | |||
+ | |||
+ | =====Testing It===== | ||
+ | <pre>protected override bool Test(Sim a, Easel target, bool isAutonomous, ref GreyedOutTooltipCallback greyedOutTooltipCallback)</pre> | ||
+ | |||
+ | * The <tt>Test()</tt> method is the special sanity check for the interaction. It will get called before the game shows a pie menu and only if <tt>Test()</tt> returns True will the interaction show up in the pie menu. | ||
+ | |||
+ | * There's an exception to the previous point: If <tt>Test()</tt> return False, but assigns a <tt>GreyedOutTooltipCallback</tt>, the interaction button will show up, but will be greyed out. But again, that goes beyond the scope of this mod. | ||
+ | |||
+ | |||
+ | ===Defining Your Own Interaction=== | ||
+ | |||
+ | Now that we have a basic idea of how interactions work, we can commence writing our own interaction. You can simply take the <tt>ScrapPaintingInteraction</tt> and change it to your liking for now. | ||
+ | |||
+ | * Change the interaction name to mirror its purpose. Don't forget to change it in the <tt>Definition</tt> as well, because that will define what interaction the InteractionDefinition will instantiate in the game. Seriously: If your InteractionDefinition points to the wrong interaction, your interaction will malfunction in a probably very obscure way. Yours truly spent more time than he'd care to admit being flabbergasted, because he simply forgot to change the declaration of the InteractionDefinition after he copy-pasted something. | ||
+ | |||
+ | * Change the target to your target class everywhere. In this tutorial that's <tt>TalkingTeddy</tt>. | ||
+ | |||
+ | * Change all the calls of LocalizeString to hard-coded strings. You shouldn't actually write interactions like that unless they're only for personal use or for stuff that isn't supposed to show up in regular gameplay. Localized coding is a topic for [http://simswiki.info/wiki.php?title=Tutorial:Sims_3_Localized_Coding another tutorial], though. | ||
+ | |||
+ | |||
+ | |||
+ | * Remove all code except the <tt>return true;</tt> part from <tt>Run()</tt>. | ||
+ | |||
+ | * Remove all code from <tt>Test()</tt> and let it <tt>return !isAutonomous</tt> instead. | ||
+ | |||
+ | After you've done all that, your interaction should look similar to this: | ||
+ | |||
+ | <pre> | ||
+ | public sealed class TalktoMe : ImmediateInteraction<Sim, TalkingTeddy> | ||
+ | { | ||
+ | public static readonly InteractionDefinition Singleton = new Definition(); | ||
protected override bool Run() | protected override bool Run() | ||
{ | { | ||
− | |||
return true; | return true; | ||
} | } | ||
− | + | [DoesntRequireTuning] | |
private sealed class Definition : ImmediateInteractionDefinition<Sim, TalkingTeddy, TalkingTeddy.TalktoMe> | private sealed class Definition : ImmediateInteractionDefinition<Sim, TalkingTeddy, TalkingTeddy.TalktoMe> | ||
− | + | { | |
− | + | protected override string GetInteractionName(Sim a, TalkingTeddy target, InteractionObjectPair interaction) | |
− | + | { | |
− | + | return "Talk To Me"; | |
− | + | } | |
− | + | protected override bool Test(Sim a, TalkingTeddy target, bool isAutonomous, ref GreyedOutTooltipCallback greyedOutTooltipCallback) | |
− | + | { | |
− | + | return !isAutonomous; | |
− | + | } | |
− | + | } | |
− | + | ||
} | } | ||
</pre> | </pre> | ||
+ | That is the basic frame of our interaction. | ||
+ | ===Making the Interaction Do Something=== | ||
+ | With the right code, interactions could do anything. They could start a fire to a whole lot like in the ChaosMagePainting. They could lock doors such as in the Lockable door. | ||
− | + | In this tutorial we will simply make the game show a notification saying "Hello!" with the thumbnail of the active sim. The game already has a method that will do that for us: | |
− | + | <pre>Sims3.Gameplay.Actors.Sim.ShowTNSIfSelectable(string, StyledNotification.NotificationStyle)</pre> | |
− | + | Note that ShowTNSIfSelectable is a non-static method, so you'll need to call it with a reference to an actual sim. Inside an interaction, we can simply reference the actor sim by calling <tt>base.Actor</tt>. | |
+ | |||
+ | The complete call is: | ||
+ | <pre>base.Actor.ShowTNSIfSelectable("Hello!", StyledNotification.NotificationStyle.kSimTalking);</pre> | ||
+ | Add it to your <tt>Run()</tt> method. | ||
+ | |||
+ | |||
+ | ===Adding The Interaction=== | ||
+ | |||
+ | We now have a complete interaction. So are we finished? Yeah, well, probably not. I mean there's a new header and all and this section has hardly begun. | ||
+ | |||
+ | All classes derived from <tt>GameObject</tt> have a method called <tt>OnStartup()</tt> that will be called after instantiating the object for the first time and then every time a world has been loaded. This is the place to add the interaction to your object. | ||
+ | |||
+ | In the code of your object, but outside of the interaction code!, simply write <tt>override</tt> in VS. VS will pop up a list of members that can be overridden. Navigate to <tt>OnStartup</tt>, press Enter and VS will quite conveniently implement the whole method for you. Add <pre>base.AddInteraction({YourInteractionName}.Singleton);</pre> to it. Instead of {YourInteractionName} you write your actual interaction's class name. Your <tt>OnStartup()</tt> method should then look similar to this: | ||
<pre> | <pre> | ||
− | base. | + | protected override void OnStartup() |
− | base. | + | { |
+ | base.OnStartup(); | ||
+ | base.AddInteraction(TalktoMe.Singleton); | ||
+ | } | ||
</pre> | </pre> | ||
− | |||
− | |||
− | + | <tt>base.OnStartup();</tt> will make sure that the <tt>OnStartup()</tt> method defined in <tt>StuffedAnimal</tt> gets called. You should have no problem identifying the call that adds the interaction to your object or what field of the interaction it uses. | |
+ | |||
+ | ===The Final Code=== | ||
+ | |||
+ | Your code should now look similar to this: | ||
+ | |||
<pre> | <pre> | ||
using System; | using System; | ||
Line 132: | Line 262: | ||
using System.Text; | using System.Text; | ||
using Sims3.Gameplay.Objects.Miscellaneous; | using Sims3.Gameplay.Objects.Miscellaneous; | ||
− | |||
using Sims3.Gameplay.Interactions; | using Sims3.Gameplay.Interactions; | ||
using Sims3.Gameplay.Actors; | using Sims3.Gameplay.Actors; | ||
using Sims3.Gameplay.Autonomy; | using Sims3.Gameplay.Autonomy; | ||
using Sims3.SimIFace; | using Sims3.SimIFace; | ||
− | |||
using Sims3.UI; | using Sims3.UI; | ||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | + | namespace Sims3.Gameplay.Objects.Miscellaneous.KolipokiMod | |
− | + | { | |
− | + | class TalkingTeddy : StuffedAnimal | |
− | + | { | |
− | + | public sealed class TalktoMe : ImmediateInteraction<Sim, TalkingTeddy> | |
− | + | { | |
− | + | public static readonly InteractionDefinition Singleton = new Definition(); | |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | protected override bool Run() | |
− | + | { | |
− | + | base.Actor.ShowTNSIfSelectable("Hello!", StyledNotification.NotificationStyle.kSimTalking); | |
− | + | return true; | |
− | + | } | |
− | + | ||
− | + | [DoesntRequireTuning] | |
− | + | private sealed class Definition : ImmediateInteractionDefinition<Sim, TalkingTeddy, TalkingTeddy.TalktoMe> | |
− | + | { | |
− | + | protected override string GetInteractionName(Sim a, TalkingTeddy target, InteractionObjectPair interaction) | |
− | + | { | |
− | + | return "Talk To Me"; | |
+ | } | ||
+ | protected override bool Test(Sim a, TalkingTeddy target, bool isAutonomous, ref GreyedOutTooltipCallback greyedOutTooltipCallback) | ||
+ | { | ||
+ | return !isAutonomous; | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | |||
+ | public override void OnStartup() | ||
+ | { | ||
+ | base.OnStartup(); | ||
+ | base.AddInteraction(TalktoMe.Singleton); | ||
+ | } | ||
+ | } | ||
+ | } | ||
</pre> | </pre> | ||
− | Save your project (again) and click on Build -> Build Solution. This will | + | Save your project (again) and click on Build -> Build Solution. This will compile your code into a library IF there are no errors. If there are errors, check your code and compare it to the code shown in this tutorial. You will find the compiled library in Documents\Visual Studio xxxx\Projects\{YourProjectName}\{YourProjectName}\bin\Release. That file is the script we need to add to our package. Whenever you change the code, don't forget to click on Build -> Build Solution again to make VS compile an updated library for you. |
− | + | In case you didn't notice: the library is the custom script earlier mentioned. | |
− | + | ||
− | + | ==Adding The Script To The Package== | |
− | + | Now to add the script to the package, we'll need to add an S3SA resource to the package to contain the script. Before we do that, we'll have to create a unique instance value for the resource. Remember: Unless you want to deliberately override an existing instance, instance values ALWAYS need to be unique. | |
− | + | ||
− | S3PE | + | Open your package in S3PE if you closed it in the meantime. |
− | + | ||
− | + | ||
+ | [[Image:OMT_HashS3SA.jpg|right|thumb|300px]] | ||
− | |||
− | |||
− | |||
− | |||
− | |||
+ | * Click on Tools -> FNV Hash. | ||
+ | * Enter a name for your script. Since the instance value must be unique, make sure that you write a unique name. Something containing your username and the name of the mod is usually a good idea. In this case it's "KolipokiMod_TalkingTeddy.dll" | ||
+ | * Click on Calculate and copy the FNV64 number. It will become the instance value of the new resource. | ||
+ | * Close the FNV Hash tool. | ||
− | |||
− | |||
− | |||
− | + | ||
+ | |||
+ | [[Image:OMT_AddS3SAResource.jpg|right|thumb|300px]] | ||
+ | |||
+ | |||
+ | |||
+ | |||
+ | * Back in the S3PE main window, click on Resource -> Add… | ||
+ | * As type choose S3SA. | ||
+ | * Enter 0 for the Group and paste the FNV64 value into the Instance field. | ||
+ | * For convenience, tick the “Use resource name” field and enter the name of the resource in the Name field. | ||
+ | * Once you’re done, click on Ok. | ||
+ | |||
+ | |||
+ | |||
+ | |||
+ | |||
+ | |||
+ | |||
+ | |||
+ | |||
+ | |||
+ | |||
+ | S3PE will now show the S3SA resource and a _KEY resource. You can just ignore the latter one. | ||
+ | |||
+ | |||
+ | |||
+ | * Right-click the S3SA resource and on the bottom of the popup window (context menu), click on Import DLL. | ||
+ | * Navigate to the .dll file VS created. It will be in Documents\Visual Studio xxxx\Projects\{YourProjectName}\{YourProjectName}\bin\Release. | ||
+ | * Select the file; click on the Open button and click on Yes once being prompted to Commit Changes. | ||
+ | |||
+ | |||
+ | |||
+ | |||
+ | [[Image:OMT_OBJK_ChangeScriptClass.jpg|right|thumb|300px]] | ||
+ | |||
+ | |||
+ | |||
+ | |||
+ | |||
+ | * Right-click the OBJK resource and on the bottom of the popup window (context menu), click on Edit OBJK | ||
+ | * Change the data field in the script row to your class name with full namespace. In case of this tutorial that's <tt>Sims3.Gameplay.Objects.Miscellaneous.KolipokiMod.TalkingTeddy</tt> | ||
+ | * Click on Save. | ||
+ | |||
+ | |||
+ | |||
+ | |||
+ | |||
+ | |||
+ | |||
+ | |||
+ | |||
+ | |||
+ | |||
+ | |||
+ | Save your package and give it a run in the game. | ||
+ | |||
+ | ==Congratulations== | ||
+ | You have now finished your object mod and should have gained a basic understanding of how to make object mods. Go get a cookie! | ||
==Video Version== | ==Video Version== | ||
− | + | The original version of this tutorial is available on video and can be found at The Sims Supply: [http://thesimsupply.com/showthread.php?tid=231 Video Tutorial]. | |
+ | |||
+ | ==Questions== | ||
+ | Ask them here: [http://www.modthesims.info/showthread.php?p=3452868 Q&A thread Sims 3 Object Modding] |
Latest revision as of 20:26, 3 February 2012
[edit] Introduction
|
While the Object Mod per se isn't limited to a specific set of properties, it is always an object that uses a custom script. The most basic sort of object mod is a simple clone of the original object rigged with some new interactions. This tutorial will show you how to clone an object, write a script for it and make the object use that script.
[edit] What You Need
- Microsoft Visual C# Express 2008 - simply called VS later in this tutorial
- (You can try using the 2010 version, but that version seems to be buggy regarding compiling against a different mscorlib.dll.)
- Sims3 Package Editor - simply called S3PE later in this tutorial
- Sims3 Object Cloner - simply called S3OC later in this tutorial
- .NET assembly browser/decompiler - this tutorial refers to redgate .NET Reflector, simply called Reflector later on
- A basic understanding of the C# syntax or at least any C-like language.
- A game that is properly set up to support scripting mods. If you fail to accomplish that, you can’t hope to successfully write object mods.
[edit] Getting Started
- Extract the core libraries with S3PE if you haven’t already. Here’s how to do that:
- Open S3PE and click on File -> Open…
- Navigate to the installation folder of The Sims 3 and from there to the sub-folder where the executable is located.
In this folder are three packages: gameplay.package, scripts.package, and simcore.package - Open one of these packages.
- Click on an S3SA resource. Note that S3PE shows some information about that resource in the preview area. Locate where it says ManifestModule. Remember what comes after the colon, e.g. Sims3GameplaySystems.dll.
- Click on Grid at the bottom of the S3PE window, and then click on the Assembly row and the little drop-down arrow on the right. Click on Export…
- Choose a sensible folder for the library and save it under the name you remembered from the ManifestModule entry.
- Repeat steps 4 to 6 for every S3SA resource in the package.
- Repeat steps 3 to 7 for every package listed under step 2.
- Close S3PE.
- Create a game-compatible Visual Studio project as explained here: Sims_3:Creating_a_game_compatible_Visual_Studio_project
- Start Reflector and load the core libraries with it.
[edit] Cloning Your Object
The next step in this process is to make your new cloned object.
- Open S3OC, Clone -> Normal Objects
- Scroll to the object to clone, select Clone or Fix button
- Enter a unique username and leave all default options checked
- Change any catalog options such as name, description, price or category
- Select Start, browse to your project folder and give the package a name
[edit] Finding The Original Object's Class
This next step is important because it will later be used to make your script be derived from the previous object. That will ensure that the new object has all the properties of the original object.
- Open the package in S3PE.
- Right-click the OBJK resource and on the bottom of the popup window (context menu), click on Edit OBJK
- Look at the content of the Data field in the Script row. That is the class name including the full namespace. (Sims3.Gameplay.Objects.Miscellaneous.StuffedAnimal in this case)
[edit] Your Custom Script
[edit] Starting Your Code
We are going to start off by changing the namespace and the class. The namespace is like the address of a class. You can have two classes having the same name as long as they're located in different namespaces. To make sure that your classes never clash with an EAxian class or with another modder's class, you need to make your namespace unique to you. Don't use an EAxian namespace or another modder's namespace! Ever.
In case of object mods, you'll always need to put an object's class in a namespace that begins with Sims3.Gameplay.Objects. If you don't do that, the object will cause the game to crash when you try to buy it in the catalog. The example namespace in this tutorial is:
namespace Sims3.Gameplay.Objects.Miscellaneous.KolipokiMod
(Remember: You need to use something different for your mod!)
Next we'll change the class name. That's not absolutely necessary, but it's not good style to give a derived class the same name as its base class. It is good style however to choose all names, be it classes or fields or whatever, in a way that they indicate what they do or stand for. Now what do you think will be the better class name for our mod? SomethingIDontKnowOrWhatever or TalkingTeddy? Right.
public class TalkingTeddy : StuffedAnimal
Now what is that "StuffedAnimal" stuff after the colon for? The colon basically means "derived from". It can also mean "implements" in case of interfaces, but we'll try to reduce the confusion to a minimum here. In this case TalkingTeddy is derived from StuffedAnimal. That is a core feature of object oriented programming and means that TalkingTeddy does not only share all properties of a StuffedAnimal, it IS in fact a StuffedAnimal while a StuffedAnimal isn't necessarily a TalkingTeddy. Think of how a weiner dog is a special type of dog, but a dog isn't necessarily a weiner dog and you'll get the idea.
For more advanced object mods, you'll derive your class from the abstract GameObject class which is the base class for all objects visible in the game, sims included. GameObject has no specific properties, so it's for the best to stick with expanding existing objects for the beginning.
The base class should be what was in the script class entry of your object. VS will show known classes in light blue, but StuffedAnimal part of your class declaraion will be shown in black. Maybe it even gives you an error like this:
Error 1 The type or namespace name 'StuffedAnimal' could not be found (are you missing a using directive or an assembly reference?)
At the top of your code, add using Sims3.Gameplay.Objects.Miscellaneous;. You'll know where. StuffedAnimal will now be shown in light blue, and if you got the stated error, it should vanish if you press F6. The using directive basically tells VS an address and to assume that everything you write might refer to that address. So if we write StuffedAnimal now, VS will assume that we in fact mean Sims3.Gameplay.Objects.Miscellaneous.StuffedAnimal, and obviously that's exactly what we mean. That works well as long as everything we write is unambiguous. If there were two StuffedAnimal classes and we used two using directives with the addresses to these classes, then VS obviously couldn't tell which one we mean when we just write StuffedAnimal. In that case we'd need to in fact use Sims3.Gameplay.Objects.Miscellaneous.StuffedAnimal to specifiy what exactly we mean.
While we're at it, add the following using directives as well, so VS knows what we mean later on.
using Sims3.Gameplay.Interactions; using Sims3.Gameplay.Actors; using Sims3.Gameplay.Autonomy; using Sims3.SimIFace; using Sims3.UI;
[edit] What's An Interaction?
Through this step we will be using Reflector so make sure you have it opened and that you have opened the libraries that we extracted in the first step. Let's have a look at how interactions are defined:
- Expand Sims3GameplayObjects -> Sims3GameplayObjects.dll -> Sims3.Gameplay.Objects.HobbiesSkills -> Easel -> ScrapPaintingInteraction
- Open the dissasembler by hitting the spacebar.
- Click on "Expand Methods" on the bottom of the Disassembler window.
The ScrapPaintingInteraction interaction is a very simple interaction. Let's look at it in more detail:
public sealed class ScrapPaintingInteraction : ImmediateInteraction<Sim, Easel> { public static readonly InteractionDefinition Singleton = new Definition(); public override bool Run() { if (base.Target.CurrentCanvas.Artist == base.Actor.SimDescription) { EventTracker.SendEvent(EventTypeId.kScrappedPainting, base.Actor); } base.Target.ScrapPainting(); return true; } [DoesntRequireTuning] private sealed class Definition : ImmediateInteractionDefinition<Sim, Easel, Easel.ScrapPaintingInteraction> { public override string GetInteractionName(Sim a, Easel target, InteractionObjectPair interaction) { return Easel.LocalizeString("ScrapPainting", new object[0x0]); } public override bool Test(Sim a, Easel target, bool isAutonomous, ref GreyedOutTooltipCallback greyedOutTooltipCallback) { return (target.CanScrap(a) && !target.InUse); } } }
Let's look at a few key parts:
[edit] Interaction Declaration
public sealed class ScrapPaintingInteraction : ImmediateInteraction<Sim, Easel>
- The interaction is declared as public sealed. Public basically means that all other code can "see" the class; sealed means that no sub classes may be derived from it.
- The class is derived from ImmediateInteraction. ImmediateInteractions are interactions that don't get added to a sim's interaction queue and run immediately no matter and not disturbing what a sim is doing. The <Sim, Easel> declares that the Actor of the interaction is a sim and the Target is an easel. Also a ImmediateInteraction can usually be recognized by the orange circle on the button in the pie menu.
[edit] Running
public override bool Run()
- The Run() method is where the actual action happens. We'll come back to that later.
[edit] Definition
[DoesntRequireTuning] private sealed class Definition : ImmediateInteractionDefinition<Sim, Easel, Easel.ScrapPaintingInteraction>
- In the game, an actual interaction is something with an actual actor and an actual target, in this case one specific sim and one specific easel. The abstract idea of an interaction that might or might not run at some point in the future, including a prospective sim and a prospective easel, is referenced by an InteractionDefinition.
- In this case Definition is derived from ImmediateInteractionDefinition which is appropriate for an ImmediateInteraction. <Sim, Easel, Easel.ScrapPaintingInteraction> isn't much different from the declaration of the Interaction, except that the last part declares it as an InteractionDefinition for a Easel.ScrapPaintingInteraction.
- The [DoesntRequireTuning] attribute tells the game that there's no ITUN resource containing tuning for this interaction, so it doesn't need to bother looking for one. Adding tuning to your interactions goes beyond the scope of this tutorial; just use the attribute whenever you write an interaction without tuning.
[edit] Singleton
public static readonly InteractionDefinition Singleton = new Definition();
- The static Singleton field is that mentioned InteractionDefinition the game uses to reference the mentioned "idea of an interaction". We'll see later how to use that.
[edit] Getting A Name
protected override string GetInteractionName(Sim a, Easel target, InteractionObjectPair interaction)
- The GetInteractionName() method gets used for the string on the interaction button in the pie menu and the mouse over text on the interaction's thumbnail in a sim's interaction queue. Of course an ImmediateInteraction doesn't show up in the queue so the latter part is JFYI.
[edit] Testing It
protected override bool Test(Sim a, Easel target, bool isAutonomous, ref GreyedOutTooltipCallback greyedOutTooltipCallback)
- The Test() method is the special sanity check for the interaction. It will get called before the game shows a pie menu and only if Test() returns True will the interaction show up in the pie menu.
- There's an exception to the previous point: If Test() return False, but assigns a GreyedOutTooltipCallback, the interaction button will show up, but will be greyed out. But again, that goes beyond the scope of this mod.
[edit] Defining Your Own Interaction
Now that we have a basic idea of how interactions work, we can commence writing our own interaction. You can simply take the ScrapPaintingInteraction and change it to your liking for now.
- Change the interaction name to mirror its purpose. Don't forget to change it in the Definition as well, because that will define what interaction the InteractionDefinition will instantiate in the game. Seriously: If your InteractionDefinition points to the wrong interaction, your interaction will malfunction in a probably very obscure way. Yours truly spent more time than he'd care to admit being flabbergasted, because he simply forgot to change the declaration of the InteractionDefinition after he copy-pasted something.
- Change the target to your target class everywhere. In this tutorial that's TalkingTeddy.
- Change all the calls of LocalizeString to hard-coded strings. You shouldn't actually write interactions like that unless they're only for personal use or for stuff that isn't supposed to show up in regular gameplay. Localized coding is a topic for another tutorial, though.
- Remove all code except the return true; part from Run().
- Remove all code from Test() and let it return !isAutonomous instead.
After you've done all that, your interaction should look similar to this:
public sealed class TalktoMe : ImmediateInteraction<Sim, TalkingTeddy> { public static readonly InteractionDefinition Singleton = new Definition(); protected override bool Run() { return true; } [DoesntRequireTuning] private sealed class Definition : ImmediateInteractionDefinition<Sim, TalkingTeddy, TalkingTeddy.TalktoMe> { protected override string GetInteractionName(Sim a, TalkingTeddy target, InteractionObjectPair interaction) { return "Talk To Me"; } protected override bool Test(Sim a, TalkingTeddy target, bool isAutonomous, ref GreyedOutTooltipCallback greyedOutTooltipCallback) { return !isAutonomous; } } }
That is the basic frame of our interaction.
[edit] Making the Interaction Do Something
With the right code, interactions could do anything. They could start a fire to a whole lot like in the ChaosMagePainting. They could lock doors such as in the Lockable door.
In this tutorial we will simply make the game show a notification saying "Hello!" with the thumbnail of the active sim. The game already has a method that will do that for us:
Sims3.Gameplay.Actors.Sim.ShowTNSIfSelectable(string, StyledNotification.NotificationStyle)
Note that ShowTNSIfSelectable is a non-static method, so you'll need to call it with a reference to an actual sim. Inside an interaction, we can simply reference the actor sim by calling base.Actor.
The complete call is:
base.Actor.ShowTNSIfSelectable("Hello!", StyledNotification.NotificationStyle.kSimTalking);
Add it to your Run() method.
[edit] Adding The Interaction
We now have a complete interaction. So are we finished? Yeah, well, probably not. I mean there's a new header and all and this section has hardly begun.
All classes derived from GameObject have a method called OnStartup() that will be called after instantiating the object for the first time and then every time a world has been loaded. This is the place to add the interaction to your object.
In the code of your object, but outside of the interaction code!, simply write override in VS. VS will pop up a list of members that can be overridden. Navigate to OnStartup, press Enter and VS will quite conveniently implement the whole method for you. Addbase.AddInteraction({YourInteractionName}.Singleton);to it. Instead of {YourInteractionName} you write your actual interaction's class name. Your OnStartup() method should then look similar to this:
protected override void OnStartup() { base.OnStartup(); base.AddInteraction(TalktoMe.Singleton); }
base.OnStartup(); will make sure that the OnStartup() method defined in StuffedAnimal gets called. You should have no problem identifying the call that adds the interaction to your object or what field of the interaction it uses.
[edit] The Final Code
Your code should now look similar to this:
using System; using System.Collections.Generic; using System.Text; using Sims3.Gameplay.Objects.Miscellaneous; using Sims3.Gameplay.Interactions; using Sims3.Gameplay.Actors; using Sims3.Gameplay.Autonomy; using Sims3.SimIFace; using Sims3.UI; namespace Sims3.Gameplay.Objects.Miscellaneous.KolipokiMod { class TalkingTeddy : StuffedAnimal { public sealed class TalktoMe : ImmediateInteraction<Sim, TalkingTeddy> { public static readonly InteractionDefinition Singleton = new Definition(); protected override bool Run() { base.Actor.ShowTNSIfSelectable("Hello!", StyledNotification.NotificationStyle.kSimTalking); return true; } [DoesntRequireTuning] private sealed class Definition : ImmediateInteractionDefinition<Sim, TalkingTeddy, TalkingTeddy.TalktoMe> { protected override string GetInteractionName(Sim a, TalkingTeddy target, InteractionObjectPair interaction) { return "Talk To Me"; } protected override bool Test(Sim a, TalkingTeddy target, bool isAutonomous, ref GreyedOutTooltipCallback greyedOutTooltipCallback) { return !isAutonomous; } } } public override void OnStartup() { base.OnStartup(); base.AddInteraction(TalktoMe.Singleton); } } }
Save your project (again) and click on Build -> Build Solution. This will compile your code into a library IF there are no errors. If there are errors, check your code and compare it to the code shown in this tutorial. You will find the compiled library in Documents\Visual Studio xxxx\Projects\{YourProjectName}\{YourProjectName}\bin\Release. That file is the script we need to add to our package. Whenever you change the code, don't forget to click on Build -> Build Solution again to make VS compile an updated library for you.
In case you didn't notice: the library is the custom script earlier mentioned.
[edit] Adding The Script To The Package
Now to add the script to the package, we'll need to add an S3SA resource to the package to contain the script. Before we do that, we'll have to create a unique instance value for the resource. Remember: Unless you want to deliberately override an existing instance, instance values ALWAYS need to be unique.
Open your package in S3PE if you closed it in the meantime.
- Click on Tools -> FNV Hash.
- Enter a name for your script. Since the instance value must be unique, make sure that you write a unique name. Something containing your username and the name of the mod is usually a good idea. In this case it's "KolipokiMod_TalkingTeddy.dll"
- Click on Calculate and copy the FNV64 number. It will become the instance value of the new resource.
- Close the FNV Hash tool.
- Back in the S3PE main window, click on Resource -> Add…
- As type choose S3SA.
- Enter 0 for the Group and paste the FNV64 value into the Instance field.
- For convenience, tick the “Use resource name” field and enter the name of the resource in the Name field.
- Once you’re done, click on Ok.
S3PE will now show the S3SA resource and a _KEY resource. You can just ignore the latter one.
- Right-click the S3SA resource and on the bottom of the popup window (context menu), click on Import DLL.
- Navigate to the .dll file VS created. It will be in Documents\Visual Studio xxxx\Projects\{YourProjectName}\{YourProjectName}\bin\Release.
- Select the file; click on the Open button and click on Yes once being prompted to Commit Changes.
- Right-click the OBJK resource and on the bottom of the popup window (context menu), click on Edit OBJK
- Change the data field in the script row to your class name with full namespace. In case of this tutorial that's Sims3.Gameplay.Objects.Miscellaneous.KolipokiMod.TalkingTeddy
- Click on Save.
Save your package and give it a run in the game.
[edit] Congratulations
You have now finished your object mod and should have gained a basic understanding of how to make object mods. Go get a cookie!
[edit] Video Version
The original version of this tutorial is available on video and can be found at The Sims Supply: Video Tutorial.
[edit] Questions
Ask them here: Q&A thread Sims 3 Object Modding