View Full Version : Help getting started with scripting
Wild Bama Boy
15th Mar 2011, 02:32 AM
Hello. I've went through the Pure Scripting Modding tutorial and had a few problems. I thought I worked them out on my own but the mod did not work. It compiles, Sims 3 sees it, but it doesn't do what I've told it to do.
Before the code I should mention what problems I had while going through the tutorial.
1. I could not add mscorlib.dll to the References. Visual Studio kept saying that it was automatically referenced in the build system, even when I told it to not reference mscorlib.dll in the Advanced Build Settings. It would not compile this way due to this error: "Predefined type 'System.Object' is not defined or imported". I just set it to reference mscorlib.dll again. I checked the versions of the one from TS3 and the .Net Framework and they matched so I figured it didn't matter...
2. This part of the tutorial confused me.
"# Start the FNV Hash tool again. This time FNV hash the namespace plus class name of the class where the tunable variable is located. In the case of this tutorial that's TwoBTech.Pausinator.
# Add another resource. The type is _XML 0x0333406C. This time the Instance value IS important."
Exactly what am I supposed to do here? What should the Instance value be? The namespace AND class name's hash? :wtf:
I set it to the hash of my namespace.
3. My XML file had the strange characters the tutorial mentioned, but I deleted them with Notepad.
Now here's my code. I tried to show a message instead of not pausing the game like the tutorial does. And I do understand C# fairly well.
using System;
using System.Collections.Generic;
using System.Text;
using Sims3.SimIFace;
namespace WildBamaBoy
{
public class ModTester
{
[Tunable]
protected static bool MakeInstance = true;
static ModTester()
{
World.OnWorldLoadFinishedEventHandler += new EventHandler(OnWorldLoadFinished);
}
private static void OnWorldLoadFinished(object sender, EventArgs e)
{
Sims3.UI.SimpleMessageDialog.Show("Hello World", "abcdefghijklmnopqrstuvwxyz");
}
}
}
lenglel
15th Mar 2011, 06:44 AM
The instance for the dll is the fnv64 hash of namespace.classname,
the tuning file instance is just the namespace. Did you set up the
s3sa resource tgi before importing with the grid button? If you
import, then set the instance, it doesn't work, I don't know why.
The code itself looks like what I've got, and mine works so that
isn't it. WRT mscorlib, what I did was set the project to not
reference it, then manually added a reference to the sims 3
version of mscorlib in my csproj file by pasting the code
below into the itemgroup section with all the other assembly
references:
<Reference Include="mscorlib">
<HintPath>..\..\..\..\s3mod projects\simcore\mscorlib.dll</HintPath>
<Private>False</Private>
</Reference>
Make sure the HintPath is correct for where you put your copy of
mscorlib.dll
Buzzler
15th Mar 2011, 10:04 AM
Exactly what am I supposed to do here? What should the Instance value be? The namespace AND class name's hash?The FullName of the class the tunable field is in, in your case the FNV64 hash of WildBamaBoy.ModTester, i.e. 0x79CA736949E3A76D.
Now here's my code. I tried to show a message instead of not pausing the game like the tutorial does.OnWorldLoadFinished is too early to do that.
Instead of
Sims3.UI.SimpleMessageDialog.Show("Hello World", "abcdefghijklmnopqrstuvwxyz");try
AlarmManager.Global.AddAlarm(1f, TimeUnit.Seconds, new AlarmTimerCallback(delegate()
{
StyledNotification.Show(new StyledNotification.Format("Hello World!", StyledNotification.NotificationStyle.kSystemMessage));
}), "Hello World Alarm", AlarmType.NeverPersisted, null);Also be careful as to *when* you show dialogs. Trying to show a dialog at the wrong time or the wrong way, may crash the game. Notifications are usually safe.
Wild Bama Boy
16th Mar 2011, 12:05 AM
Alright! It finally worked. I've tried this so many times before and never managed to get it. Thank you to both of you.
Now I have two more questions. Is there any limitation on what I can code in the dll file? Can I have multiple classes, functions, etc. and will they work correctly?
And also how would I go about adding an interaction to an existing object?
Buzzler
16th Mar 2011, 10:39 AM
Now I have two more questions. Is there any limitation on what I can code in the dll file? Can I have multiple classes, functions, etc. and will they work correctly?You can basically do whatever you want however you want. A couple things, like direct file access, are stripped from the mscorlib shipped with TS3, though.
And also how would I go about adding an interaction to an existing object?If you already know the object, and it's a GameObject (basically everything visible in the game is), you simply call its AddInteraction() method:gameObject.AddInteraction(InteractionDefinition); If you want to add an interaction to all objects of a certain type, you call Sims3.Gameplay.Queries.GetObjects<T>(); I suggest you look at other people's mods to cover the basics. I'd advise against looking at twallan's mods to start, though. ;) He's using a big-ish custom framework now that tends to overshadow what of a mod is actually TS3-specific.
Wild Bama Boy
16th Mar 2011, 11:28 PM
Thank you very much. I should be able to do the rest on my own now.
Wild Bama Boy
18th Mar 2011, 04:52 AM
Gah. Very frustrating. I tried to add a custom interaction to the school building and that never would work...then I tried to add a Writing class to the school after finding its code. But it doesn't work at all either. What am I doing wrong? SchoolRabbitHole didn't have a definition of AddInteraction until I inherited it with another class. Inheritance and different kinds of classes are newish to me, so I don't really know what I'm talking about. :P
using System;
using System.Collections.Generic;
using System.Text;
using Sims3.Gameplay.Actors;
using Sims3.Gameplay.Interactions;
using Sims3.Gameplay.Objects.RabbitHoles;
using Sims3.Gameplay.Interfaces;
using Sims3.Gameplay.Autonomy;
using Sims3.SimIFace;
using Sims3.UI;
using Sims3.Gameplay.Utilities;
using Sims3.Gameplay.Abstracts;
namespace WildBamaBoy
{
public class TestMod
{
[Tunable]
protected static bool MakeInstance = true;
static TestMod()
{
World.OnWorldLoadFinishedEventHandler += new EventHandler(OnWorldLoadFinished);
}
private static void OnWorldLoadFinished(object sender, EventArgs e)
{
//So I know that it is working...
AlarmManager.Global.AddAlarm(1f, TimeUnit.Seconds, new AlarmTimerCallback(delegate()
{
StyledNotification.Show(new StyledNotification.Format("Test", StyledNotification.NotificationStyle.kSystemMessage));
}), "Test Alarm", AlarmType.NeverPersisted, null);
School s = new School();
}
class School : SchoolRabbitHole
{
public School()
{
AlarmManager.Global.AddAlarm(1f, TimeUnit.Seconds, new AlarmTimerCallback(delegate()
{
StyledNotification.Show(new StyledNotification.Format("started", StyledNotification.NotificationStyle.kSystemMessage));
}), "a", AlarmType.NeverPersisted, null);
base.AddInteraction(new RabbitHole.AttendClassInRabbitHole.Definition(Sims3.Gameplay.Skills.SkillNames.Writing, 500, 1000));
}
}
}
}
Buzzler
18th Mar 2011, 09:22 AM
Gah. Very frustrating. I tried to add a custom interaction to the school building and that never would work...Let's see that interaction. ;)
SchoolRabbitHole didn't have a definition of AddInteraction until I inherited it with another class.You won't find an AddInteraction() method in the code of SchoolRabbitHole, correct. That's because it doesn't need one; it's part of GameObject. SchoolRabbitHole is derived from RabbitHole which is derived from GameObject. Thus SchoolRabbitHole IS_A GameObject and can be used as such.
In your OnWorldLoaded() method, call:
foreach (SchoolRabbitHole hole in Sims3.Gameplay.Queries.GetObjects<SchoolRabbitHole>())
{
hole.AddInteraction(new RabbitHole.AttendClassInRabbitHole.Definition(Sims3.Gameplay.Skills.SkillNames.Writing, 500, 1000));
// Actually, don't do that; it's just to show the principle
}
Instead of adding an interaction right away, you need to check if the interaction is there already. Otherwise you'll end up with duplicate interactions. Example ripped out from one of my mods:
private static void AddInteraction(Terrain terrain)
{
if (terrain == null)
{
return false;
}
foreach (InteractionObjectPair iop in terrain.Interactions)
{
if (iop.InteractionDefinition.GetType() == DigSiteFixer.FindAllDigSites.Singleton)
{
return;
}
}
terrain.AddInteraction(DigSiteFixer.FindAllDigSites.Singleton);
}It will be a bit more difficult in this case, because any code that simply looks for typeof(RabbitHole.AttendClassInRabbitHole.Definition) will of course find a match in the school's interactions: the painting class interaction.
Also, I think the Singleton approach in the EAxian code, one public static InteractionDefinition field and inaccessible constructors, is pretty good. I'd do it the same way instead of instantiating new InteractionDefinitions all over the place, or even allowing to do that.
School s = new School();
Unfortunately, that doesn't create an actual school. Only the code of the school which is pretty much useless on its own.
class School : SchoolRabbitHole
That will derive a class from SchoolRabbitHole, yes, but none of the schools will use it just like that.
Wild Bama Boy
19th Mar 2011, 01:44 AM
Alright! I understand now, great post. And now I've gotten my interaction to work, but I need a way to keep up with things.
Here is my interaction...
private sealed class School
{
public sealed class AttendHomeschoolingClass : Interaction<Sim, SchoolRabbitHole>
{
public static readonly InteractionDefinition Singleton = new Definition();
protected override bool Run()
{
base.Actor.SwitchToOutfitWithSpin(Sim.ClothesChangeReason.GoingToRabbitHole, OutfitCategories.Everyday);
//Null values are RabbitHoleFollowers and BeforeEnteringRabbitHoleDelegate. Bad thing to do?
Target.RouteNearEntranceAndEnterRabbitHole(base.Actor, null, null, true, Route.RouteMetaType.GoRabbitHole, false);
base.Actor.ShowTNSIfSelectable(Actor.Name + " is attending a Homeschooling class.", StyledNotification.NotificationStyle.kSimTalking);
base.CancellableByPlayer = false;
base.Actor.ModifyFunds(-1000);
base.Actor.WaitForExitReason(60, ExitReason.StageComplete);
base.Actor.AddExitReason(ExitReason.StageComplete);
Target.ExitRabbitHoleAndRouteAway(Actor);
return true;
}
[DoesntRequireTuning]
private sealed class Definition : InteractionDefinition<Sim, SchoolRabbitHole, School.AttendHomeschoolingClass>
{
protected override string GetInteractionName(Sim sim, SchoolRabbitHole target, InteractionObjectPair interaction)
{
return "Attend Homeschooling Class ($200)";
}
protected override bool Test(Sim sim, SchoolRabbitHole target, bool isAutonomous, ref GreyedOutTooltipCallback greyedOutTooltipCallback)
{
return !isAutonomous;
}
}
}
}
I want the class to be available to adults and elders only and I want them to have to take it 5 times before they can't anymore. That's when the main part of this mod will activate. But how can I keep up with how many times each individual Sim has taken the class?
Buzzler
19th Mar 2011, 07:52 AM
All right, there's still quite a bit to do. I don't have that much time right now, so I'll only mention a few key parts. Maybe I can post a "framework" later.
public sealed class AttendHomeschoolingClass : Interaction<Sim, SchoolRabbitHole>
There's already a RabbitHoleInteraction class in the code that will do a lot of the tedious stuff for you.
Target.RouteNearEntranceAndEnterRabbitHole(base.Actor, null, null, true, Route.RouteMetaType.GoRabbitHole, false);All the routing methods are boolean return types that return false on failure. You should always do something like that:
if (!base.Target.RouteNearEntranceAndEnterRabbitHole(base.Actor, null, null, true, Route.RouteMetaType.GoRabbitHole, false))
{
return false;
}BTW: If you use a RabbitHoleInteraction, you don't need to do the routing yourself.
base.Actor.WaitForExitReason(60, ExitReason.StageComplete);If you want to use stages, you need to set them up yourself. In this particular case, you probably want a TimedStage. You can instantiate a new TimedStage in ConfigureInteraction() (or BeforeEnteringRabbithole() for the RabbitHoleInteraction) and then assign base.Stages. When you want the timer to start, in Run() or InRabbithole(), you call base.StartStages() (iirc).
I'd suggest to use that "bool succeeded = base.DoLoop()" approach you'll see in a lot of EAxian RabbitHoleInteractions.
I want the class to be available to adults and elders onlyAdd a check for it in Test() or add an ITUN resource for the interaction to your mod.
and I want them to have to take it 5 times before they can't anymore. That's when the main part of this mod will activate. But how can I keep up with how many times each individual Sim has taken the class?You can simply store the times a sim has taken a class in a static dictionary somewhere. I'd recommend Dictionary<ulong, int> with the sims' SimDescriptionId as key. If you want static fields to survive save&reload, use the [PersistableStatic] attribute on them.
Wild Bama Boy
20th Mar 2011, 01:57 AM
Ok now I've got my classes working correctly.
If you want static fields to survive save&reload, use the [PersistableStatic] attribute on them.
If I code this like so:
private static class HomeschoolingClassesTaken
{
[PersistableStatic]
public static Dictionary<ulong, int> Dict = new Dictionary<ulong, int>();
Won't the old one be erased when a saved game is loaded?
And also, now I need to know how to make a menu of choices. Like the one that comes up with different book genres when you're going to write one on the computer. I've looked at that specific interaction but I don't understand how it's doing that. I want mine to initially say "Register Children for Homeschooling..." and then go to a list of eligible children (I've already got the list) that you can click on. How do I make these groups?
Buzzler
20th Mar 2011, 12:19 PM
If I code this like so:{...}
Won't the old one be erased when a saved game is loaded?I guess it will. Simply check if it's null, before you assign a new Dictionary to it. I'd make the dictionary private to begin with and add public Add/Get/Modify/whatever methods for your interaction(s) to access it.
I've looked at that specific interaction but I don't understand how it's doing that. I want mine to initially say "Register Children for Homeschooling..." and then go to a list of eligible children (I've already got the list) that you can click on. How do I make these groups?There are two ways:
1) One button in the piemenu for every choice. For that, you need a field in your InteractionDefinition to store the choice and an additional constructor with the related parameter. Then you override the AddInteractions() method and add one InteractionObjectPair for each possible choice.
Example:private sealed class Definition : InteractionDefinition<Sim, Something>
{
public Sim selectedSim;
internal Definition(){}
private Definition(Sim selectedSim)
{
this.selectedSim = selectedSim;
}
protected override void AddInteractions(etc.)
{
// Get your List of Sims
foreach(Sim sim in yourListOfTargetSims)
{
results.Add(new InteractionObjectPair(new Definition(sim), target));
}
}
// The other stuff like Test() etc.
}
In the interaction you can access that field, by casting base.InteractionDefinition into a Definition, i.e. (base.InteractionDefinition as Definition).selectedSim.
2) Using a PieMenuPicker like in the Restaurant's EatHere->With Others interaction. For that override PopulatePieMenuPicker(etc.) in your InteractionDefinition. It gets incredibly easy if you want to select from a list of sims, because you can simply call base.PopulateSimPicker().
Example:public override void PopulatePieMenuPicker(ref InteractionInstanceParameters parameters, out List<ObjectPicker.TabInfo> listObjs, out List<ObjectPicker.HeaderInfo> headers, out int NumSelectableRows)
{
NumSelectableRows = -1; // number of items that may be selected from the list or -1 for no limit
base.PopulateSimPicker(ref parameters, out listObjs, out headers, *YourListOfSimsHere*, false);
}In a RabbitHoleInteraction, you can access the selected sim by base.SimFollowers. In a regular interaction, you access them with base.SelectedObjects and cast them.
Wild Bama Boy
20th Mar 2011, 10:06 PM
I'm going with the easier one, #2.
Whatever I've done has caused the school to not show any interactions when clicked on, even the original ones. I want to click Register Children for Homeschooling and then have the pie menu come up after the sim has entered the school. I think I'm doing 2 things wrong.
My list of sims is actually a list of their descriptions, because the only way I could find the children of a Sim returned them as a "Genealogy". So I put their SimDescriptions in a list and returned them. I feel like this is completely wrong...
And how do I make the populate pie menu show up after the sim is in the building?
Buzzler
20th Mar 2011, 11:11 PM
Whatever I've done has caused the school to not show any interactions when clicked on, even the original ones.One of your calls is causing an exception, probably in Test(). At least that's where I usually find the cause. ;)
I want to click Register Children for Homeschooling and then have the pie menu come up after the sim has entered the school.Ok, if you want to show the ObjectPickerDialog when the interaction is already running, it gets more difficult, because you have to "build" and show it yourself. Let me dig a little if I can find something that could work as a template somewhere in one of my mods... here, that should do it:internal static List<SimDescription> ShowSimPickerList(List<SimDescription> targets, string title)
{
List<ObjectPicker.HeaderInfo> headers = new List<ObjectPicker.HeaderInfo>();
headers.Add(new ObjectPicker.HeaderInfo("", null, 350));
List<ObjectPicker.RowInfo> rowinfoList = new List<ObjectPicker.RowInfo>();
foreach (SimDescription sd in targets)
{
ObjectPicker.RowInfo rowinfo = new ObjectPicker.RowInfo(sd, new List<ObjectPicker.ColumnInfo>());
rowinfo.ColumnInfo.Add(new ObjectPicker.ThumbAndTextColumn(sd.GetThumbnailKey(ThumbnailSize.Medium, 0), sd.FullName));
}
List<ObjectPicker.TabInfo> listObjs = new List<ObjectPicker.TabInfo>();
listObjs.Add(new ObjectPicker.TabInfo("shop_all_r2", Localization.LocalizeString("2BTech/ChokerMod:MenuHeader", new object[0]), rowinfoList));
string buttonTrue = Localization.LocalizeString("Ui/Caption/Global:Ok");
string buttonFalse = Localization.LocalizeString("Ui/Caption/Global:Cancel");
List<ObjectPicker.RowInfo> victims = ObjectPickerDialog.Show(title, buttonTrue, buttonFalse, listObjs, headers, -1);
if ((victims == null) || (victims.Count < 1))
{
return null;
}
List<SimDescription> list = new List<SimDescription>();
foreach (ObjectPicker.RowInfo info in victims)
{
list.Add(info.Item as SimDescription);
}
return list;
}
My list of sims is actually a list of their descriptions, because the only way I could find the children of a Sim returned them as a "Genealogy". So I put their SimDescriptions in a list and returned them.The PopulateSimPicker thing only works with sims. You can access the Sim belonging to a SimDescription by calling SimDescription.CreatedSim. If the sim isn't up and running at the time that will return null, though, so you have to check for that. The PopulateSimPicker thing in the InteractionDefinition gets called before the interaction actually starts, though, so this is JFYI anyway.
And how do I make the populate pie menu show up after the sim is in the building?If you use a RabbitHoleInteraction, use the method quoted above and call it in InRabbitHole().
Wild Bama Boy
21st Mar 2011, 04:13 AM
Oh wow...maybe it would be better to show the SimPicker thing after clicking "Register children" instead of going to the school and then showing it. That won't work the way I thought it would. Sorry about that. :\ And what do you mean by "if the sim is up and running" ?
I'm going to attempt this again so don't help yet!
EDIT: I don't know how to show it after the button is clicked! :faceslap: Sorry if it's up there but I didn't see it. How do I make the SimPicker show up after I click my Register children button? This is my last problem BTW, the rest I'm going to attempt myself and if I can't do it, oh well.
lenglel
21st Mar 2011, 11:03 AM
selectedSim = (Sim)ComboSelectionDialog.Show((Localize("Which child does") + theActor.Name + Localize(" want to register?")), localDict, obj);
Would something like that work, where theActor is the parent doing the registering, localDict is a dictionary of the kid's names paired with their
instances, and obj is the default selection?
Buzzler
21st Mar 2011, 07:54 PM
Oh wow...maybe it would be better to show the SimPicker thing after clicking "Register children" instead of going to the school and then showing it.That really depends on what you want to do and how the interaction is supposed to appear. I wouldn't have written the "Call For Service" interaction like the devs did for example. You choose whom to call first and THEN you dial, not the other way around. I hope you see what I'm getting at. :)
And what do you mean by "if the sim is up and running" ?Sims are more or less just the visible avatars. What defines a sim is stored in the SimDescription (and its base classes and members). So it's perfectly possible to have a SimDescription that has currently no avatar. That's the case for ServiceNPCs and homeless sims most of the time for example. If you let a sim invite a homeless friend, the game will instantiate a Sim avatar for the SimDescription somewhere out of sight and get them going. The same applies for ServiceNPCs. That's why you can't just assume that there's a Sim for every SimDescription and need to check whether SimDescription.CreatedSim returns null or not.
EDIT: I don't know how to show it after the button is clicked! :faceslap: Sorry if it's up there but I didn't see it. How do I make the SimPicker show up after I click my Register children button?That would be the "public override void PopulatePieMenuPicker" (it's actually "protected" by default) stuff I wrote about.
And JFYI: It took me quite a while to "naturally" use all that stuff like I do now. TS3 is a massive heap of undocumented code after all, so don't give up yet! ;)
Would something like that work, where theActor is the parent doing the registering, localDict is a dictionary of the kid's names paired with their instances, and obj is the default selection?The ComboSelectionDialog seems to be used only by one specific method. The string key might be the key to a resource or something. I don't know if it can be used for different stuff. You're welcome to try. ;)
Wild Bama Boy
22nd Mar 2011, 06:34 AM
That would be the "public override void PopulatePieMenuPicker" (it's actually "protected" by default) stuff I wrote about.
I have that in my Definition class, which is inside my RegisterChildForHomeschooling class, which is a RabbitHoleInteraction. It was causing an exception, "Null value found where an object instance was required." or something like that. I'm not where I can test this at the moment.
If it wasn't causing an exception, I still don't understand how it's supposed to appear after I click my Register Children button...and then my Sim (the parent) go to the school after I pick one. How is that supposed to work?
lenglel
22nd Mar 2011, 10:51 AM
Buzzler: The string key in the ComboSelectionDialog is the text that appears in the
dialog, the object associated with it is what is returned by the function if the line
with that text is selected. I like it because its tidier than yet another pie menu.
That, and I have no experience with the PopulatePieMenuPicker. This is quite
an informative discussion, as I'm planning one or two rabbit hole interactions
myself now that my sims can pick up and put down my custom objects
without leaving them hanging in mid-air as we discussed in the other thread.
Buzzler
22nd Mar 2011, 07:01 PM
I have that in my Definition class, which is inside my RegisterChildForHomeschooling class, which is a RabbitHoleInteraction. It was causing an exception, "Null value found where an object instance was required." Feel free to post it. :)
If it wasn't causing an exception, I still don't understand how it's supposed to appear after I click my Register Children button...and then my Sim (the parent) go to the school after I pick one. How is that supposed to work?The PickerDialog will show like an "extension" of the PieMenu, e.g. like the Eat Here interaction at Restaurants.
I was wrong about what I told you about base.SimFollowers, though. The selection from the PieMenuPicker gets added to the Interaction's (m)SelectedObjects which is a List<object>. Its contents needs to be added to SimFollowers manually if the sims in question are supposed to participate. That's a good thing if you only want to know the selected sims, and don't want to "push them around".
I like it because its tidier than yet another pie menu.So you already tested it? It can only show strings, though and doesn't allow to select multiple options.
BTW: You know that "(Localize("Which child does") + theActor.Name + Localize(" want to register?")" is ugly, right?
lenglel
24th Mar 2011, 01:16 AM
Yes, I see what you mean. He needs to multi-select enrollees, and my method can only select one item from a list.
Think I'll just go back to lurk mode on this one...
vBulletin v3.0.14, Copyright ©2000-2013, Jelsoft Enterprises Ltd.