Compare commits

..

No commits in common. "main" and "0.27.0" have entirely different histories.
main ... 0.27.0

14 changed files with 170 additions and 165 deletions

View file

@ -3,10 +3,10 @@
"isRoot": true,
"tools": {
"cake.tool": {
"version": "3.0.0",
"version": "2.2.0",
"commands": [
"dotnet-cake"
]
}
}
}
}

4
.gitignore vendored
View file

@ -1,6 +1,4 @@
bin/
obj/
/packages/
.idea/
.vs/
.vscode/
.idea

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -1,8 +1,7 @@
{
"BuildMode": {
"ExampleMod.CustomTable": "Custom Table",
"ExampleMod.CrossedWallpaper": "Crossed Wallpaper",
"ExampleMod.CustomTile": "Custom Tile"
"ExampleMod.CrossedWallpaper": "Crossed Wallpaper"
},
"Clothes": {
"ExampleMod.DarkShirt": "Dark Shirt",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 535 B

37
CustomTable.cs Normal file
View file

@ -0,0 +1,37 @@
using System;
using System.Runtime.Serialization;
using TinyLife.Objects;
using TinyLife.World;
namespace ExampleMod;
// note that having a custom class for a furniture item like this is entirely optional
// but it allows for additional functionalities as displayed in this example
public class CustomTable : Furniture {
// anything whose base classes have the DataContract attribute automatically gets saved and loaded to and from disk
// this means that you can add custom DataMember members to have them saved and loaded
[DataMember]
public float TestValue;
public CustomTable(Guid id, FurnitureType type, int[] colors, Map map, Vector2 pos) : base(id, type, colors, map, pos) {
this.TestValue = Furniture.Random.NextSingle();
}
public override void OnAdded() {
base.OnAdded();
ExampleMod.Logger.Info("The custom table was added at " + this.Position);
}
public override void OnRemoved() {
base.OnRemoved();
ExampleMod.Logger.Info("The custom table was removed from " + this.Position);
}
// validate is called when this object is loaded from disk
// returning false causes the object to be marked as invalid and removed
public override bool Validate() {
return base.Validate() && this.TestValue <= 1;
}
}

View file

@ -1,10 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using ExtremelySimpleLogger;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using MLEM.Data;
using MLEM.Data.Content;
using MLEM.Textures;
@ -17,7 +13,6 @@ using TinyLife.Mods;
using TinyLife.Objects;
using TinyLife.Utilities;
using TinyLife.World;
using Action = TinyLife.Actions.Action;
namespace ExampleMod;
@ -25,46 +20,27 @@ public class ExampleMod : Mod {
// the logger that we can use to log info about this mod
public static Logger Logger { get; private set; }
public static ExampleOptions Options { get; private set; }
public static ModOptions Options { get; private set; }
public static EmotionModifier GrassSittingModifier { get; private set; }
// visual data about this mod
public override string Name => "Example Mod";
public override string Description => "This is the example mod for Tiny Life!";
public override TextureRegion Icon => this.uiTextures[new Point(0, 0)];
public override string IssueTrackerUrl => "https://github.com/Ellpeck/TinyLifeExampleMod/issues";
public override string TestedVersionRange => "[0.42.0,0.42.4]";
public override TextureRegion Icon => this.uiTextures[0, 0];
private Dictionary<Point, TextureRegion> customTops;
private Dictionary<Point, TextureRegion> customHairs;
private Dictionary<Point, TextureRegion> customBottoms;
private Dictionary<Point, TextureRegion> uiTextures;
private UniformTextureAtlas customTops;
private UniformTextureAtlas customHairs;
private UniformTextureAtlas customBottoms;
private UniformTextureAtlas uiTextures;
private Dictionary<Point, TextureRegion> wallpaperTextures;
private Dictionary<Point, TextureRegion> tileTextures;
public override void Initialize(Logger logger, RawContentManager content, RuntimeTexturePacker texturePacker, ModInfo info) {
ExampleMod.Logger = logger;
ExampleMod.Options = info.LoadOptions(() => new ExampleOptions());
// loads a texture atlas with the given amount of separate texture regions in the x and y axes
// we submit it to the texture packer to increase rendering performance. The callback is invoked once packing is completed
// additionally, we pad all texture regions by 1 pixel, so that rounding errors during rendering don't cause visual artifacts
texturePacker.Add(new UniformTextureAtlas(content.Load<Texture2D>("CustomTops"), 4, 11), r => this.customTops = r, 1, true);
texturePacker.Add(new UniformTextureAtlas(content.Load<Texture2D>("CustomHairs"), 4, 5), r => this.customHairs = r, 1, true);
texturePacker.Add(new UniformTextureAtlas(content.Load<Texture2D>("CustomBottomsShoes"), 8, 6), r => this.customBottoms = r, 1, true);
texturePacker.Add(new UniformTextureAtlas(content.Load<Texture2D>("UiTextures"), 8, 8), r => this.uiTextures = r, 1, true);
texturePacker.Add(new UniformTextureAtlas(content.Load<Texture2D>("Tiles"), 4, 2), r => this.tileTextures = r, 1, true);
// wallpaper textures require special treatment to work with openings, the x and y values are passed to the UniformTextureAtlas constructor
WallMode.ApplyMasks(content.Load<Texture2D>("Wallpapers"), 4, 5, texturePacker, r => this.wallpaperTextures = r);
}
public override void AddGameContent(GameImpl game, ModInfo info) {
// adding a custom furniture item
FurnitureType.Register(new FurnitureType.TypeSettings("ExampleMod.CustomTable", new Point(1, 1), ObjectCategory.Table, 150, ColorScheme.SimpleWood) {
// specify the type that should be constructed when this furniture type is placed
// if this is not specified, the Furniture class is used, which is used for furniture without special animations or data
ConstructedType = typeof(ExampleTable),
ConstructedType = typeof(CustomTable),
// specifying icons for custom clothes and furniture is optional, but using the mod's icon helps users recognize a mod's features
Icon = this.Icon,
// allow chairs and plates to be slotted into and onto the table
@ -73,25 +49,15 @@ public class ExampleMod : Mod {
// adding custom clothing
var darkShirt = new Clothes("ExampleMod.DarkShirt", ClothesLayer.Shirt,
// the top left in-world region
// additional regions will be auto-gathered from the atlas according to the rules described in https://docs.tinylifegame.com/articles/creating_textures.html
this.customTops, new Point(0, 0),
// the price
100,
// the clothes item's use cases
ClothesIntention.Everyday | ClothesIntention.Workout,
// the clothes item's style preferences, which influence randomly generated tinies slightly
// neutral style preferences have the same chance to be picked for all tinies, others have a 25% chance for mismatched preferences
StylePreference.Neutral,
// the clothes item's color scheme
// if the item should have multiple layers, multiple color schemes can be supplied here (see docs above)
ColorScheme.WarmDark
) {Icon = this.Icon};
this.customTops[0, 0], // the top left in-world region (the rest will be auto-gathered from the atlas)
100, // the price
ClothesIntention.Everyday | ClothesIntention.Workout, // the clothes item's use cases
ColorScheme.WarmDark) {Icon = this.Icon};
Clothes.Register(darkShirt);
// adding some more custom clothing
Clothes.Register(new Clothes("ExampleMod.PastelPants", ClothesLayer.Pants, this.customBottoms, new Point(4, 0), 100, ClothesIntention.Everyday, StylePreference.Neutral, ColorScheme.Pastel) {Icon = this.Icon});
Clothes.Register(new Clothes("ExampleMod.PastelShoes", ClothesLayer.Shoes, this.customBottoms, new Point(0, 0), 100, ClothesIntention.Everyday, StylePreference.Neutral, ColorScheme.Pastel) {Icon = this.Icon});
Clothes.Register(new Clothes("ExampleMod.WeirdHair", ClothesLayer.Hair, this.customHairs, new Point(0, 0), 0, ClothesIntention.None, StylePreference.Neutral, ColorScheme.Modern) {Icon = this.Icon});
Clothes.Register(new Clothes("ExampleMod.PastelPants", ClothesLayer.Pants, this.customBottoms[4, 0], 100, ClothesIntention.Everyday, ColorScheme.Pastel) {Icon = this.Icon});
Clothes.Register(new Clothes("ExampleMod.PastelShoes", ClothesLayer.Shoes, this.customBottoms[0, 0], 100, ClothesIntention.Everyday, ColorScheme.Pastel) {Icon = this.Icon});
Clothes.Register(new Clothes("ExampleMod.WeirdHair", ClothesLayer.Hair, this.customHairs[0, 0], 0, ClothesIntention.None, ColorScheme.Modern) {Icon = this.Icon});
// adding an event subscription to people
MapObject.OnEventsAttachable += o => {
@ -105,12 +71,12 @@ public class ExampleMod : Mod {
};
// adding a simple action: sitting down in the grass, which also gives us a nice emotion modifier
ActionType.Register(new ActionType.TypeSettings("ExampleMod.SitOnGrass", ObjectCategory.Ground, typeof(ExampleGrassSitAction)) {
ActionType.Register(new ActionType.TypeSettings("ExampleMod.SitOnGrass", ObjectCategory.Ground, typeof(SitDownOnGrassAction)) {
// we set this action to be executable only on grass tiles, not on other ground
CanExecute = (actionInfo, _) => {
if (!actionInfo.GoalMap.IsInBounds(actionInfo.ActionLocation.ToPoint()))
if (!actionInfo.Map.IsInBounds(actionInfo.ActionLocation.ToPoint()))
return CanExecuteResult.Hidden;
var tile = actionInfo.GoalMap.GetTile(actionInfo.ActionLocation.ToPoint(), (int) actionInfo.ActionFloor);
var tile = actionInfo.Map.GetTile(actionInfo.ActionLocation.ToPoint());
// hidden means the action won't be displayed in the ring menu, Valid means the player (or AI) is able to enqueue and execute it
return tile.Name.StartsWith("Grass") ? CanExecuteResult.Valid : CanExecuteResult.Hidden;
},
@ -118,23 +84,34 @@ public class ExampleMod : Mod {
// we allow the action to be done even if the solved needs aren't low enough on a person
CanDoRandomly = true,
// the solved needs indicate when the AI should mark this action as important, they don't actually have to match the action's behavior
SolvedNeeds = [NeedType.Energy],
SolvedNeeds = new[] {NeedType.Energy},
// make people more likely to sit down in the grass if they're uncomfortable
PassivePriority = p => p.Emotion == EmotionType.Uncomfortable ? 150 : 25
},
// since this action doesn't use objects (like chairs etc.), we set a texture to display instead
Texture = this.uiTextures[new Point(1, 0)]
Texture = this.uiTextures[1, 0]
});
// we use this emotion modifier in SitDownOnGrassAction
ExampleMod.GrassSittingModifier = EmotionModifier.Register(
new EmotionModifier("ExampleMod.GrassSitting", this.uiTextures[new Point(1, 0)], EmotionType.Happy));
new EmotionModifier("ExampleMod.GrassSitting", this.uiTextures[1, 0], EmotionType.Happy));
// adding a custom wallpaper (we're using the top left texture region, which is why we pass 0, 0 as the texture coordinate)
Wallpaper.Register("ExampleMod.CrossedWallpaper", 15, this.wallpaperTextures, new Point(0, 0), ColorScheme.Modern, this.Icon);
}
// adding a custom tile
Tile.Register("ExampleMod.CustomTile", 8, this.tileTextures, new Point(0, 0), ColorScheme.Bricks, icon: this.Icon);
public override void Initialize(Logger logger, RawContentManager content, RuntimeTexturePacker texturePacker, ModInfo info) {
ExampleMod.Logger = logger;
ExampleMod.Options = info.LoadOptions(() => new ModOptions());
// loads a texture atlas with the given amount of separate texture regions in the x and y axes
// we submit it to the texture packer to increase rendering performance. The callback is invoked once packing is completed
texturePacker.Add(content.Load<Texture2D>("CustomTops"), r => this.customTops = new UniformTextureAtlas(r, 4, 8));
texturePacker.Add(content.Load<Texture2D>("CustomHairs"), r => this.customHairs = new UniformTextureAtlas(r, 4, 6));
texturePacker.Add(content.Load<Texture2D>("CustomBottomsShoes"), r => this.customBottoms = new UniformTextureAtlas(r, 8, 6));
texturePacker.Add(content.Load<Texture2D>("UiTextures"), r => this.uiTextures = new UniformTextureAtlas(r, 8, 8));
// wallpaper textures require special treatment to work with openings, the x and y values are passed to the UniformTextureAtlas constructor
WallMode.ApplyMasks(content.Load<Texture2D>("Wallpapers"), 4, 5, texturePacker, r => this.wallpaperTextures = r);
}
public override IEnumerable<string> GetCustomFurnitureTextures(ModInfo info) {
@ -150,70 +127,13 @@ public class ExampleMod : Mod {
// in general, options should be stored in the ModInfo.OptionsFile file that is given to the mod by the game
public override void PopulateOptions(Group group, ModInfo info) {
group.AddChild(new Paragraph(Anchor.AutoLeft, 1, _ => $"{Localization.Get(LnCategory.Ui, "ExampleMod.DarkShirtSpeedOption")}: {ExampleMod.Options.DarkShirtSpeedIncrease}"));
group.AddChild(new Slider(Anchor.AutoLeft, new Vector2(1, 10), 5, 5) {
group.AddChild(new Slider(Anchor.AutoLeft, new Vector2(1, 12), 5, 5) {
CurrentValue = ExampleMod.Options.DarkShirtSpeedIncrease,
OnValueChanged = (_, v) => ExampleMod.Options.DarkShirtSpeedIncrease = v
OnValueChanged = (_, v) => {
ExampleMod.Options.DarkShirtSpeedIncrease = v;
info.SaveOptions(ExampleMod.Options);
}
});
group.OnRemovedFromUi += _ => info.SaveOptions(ExampleMod.Options);
}
}
// these options are saved and loaded in ExampleMod
public class ExampleOptions {
public float DarkShirtSpeedIncrease = 2;
}
// we use a multi action because we want to walk to the location, and then execute the main sitting part
// see ExampleTable for information on how to store custom action-specific information to disk as well
public class ExampleGrassSitAction : MultiAction {
public ExampleGrassSitAction(ActionType type, ActionInfo info) : base(type, info) {}
protected override IEnumerable<Action> CreateFirstActions() {
// we want to walk to the location clicked, so we use the current action info
yield return ActionType.GoHere.Construct<Action>(this.Info);
}
protected override void AndThenUpdate(GameTime time, TimeSpan passedInGame, float speedMultiplier) {
base.AndThenUpdate(time, passedInGame, speedMultiplier);
// set our person to look like they're sitting on the ground
this.Person.CurrentPose = Pose.SittingGround;
// restore need and lower emotions
this.Person.RestoreNeed(NeedType.Energy, 0.5F, this.Info, speedMultiplier);
this.Person.LowerEmotion(EmotionType.Uncomfortable, 0.0001F, speedMultiplier);
}
protected override CompletionType AndThenIsCompleted() {
// we want to complete our action once 10 minutes of sitting time have passed
return this.CompleteIfTimeUp(TimeSpan.FromMinutes(10));
}
protected override void AndThenOnCompleted(CompletionType type) {
base.AndThenOnCompleted(type);
// this method is called when the action completes in any way, even if it fails
if (type == CompletionType.Completed) {
// once we're finished sitting, we want to get a nice emotion modifier for it
this.Person.AddEmotion(ExampleMod.GrassSittingModifier, 2, TimeSpan.FromHours(1), this.Type);
}
}
}
// note that having a custom class for a furniture item like this is entirely optional
// but it allows for additional functionalities as displayed in this example
public class ExampleTable : Furniture {
// anything whose base classes have the DataContract attribute automatically gets saved and loaded to and from disk
// this means that you can add custom DataMember members to have them saved and loaded
[DataMember]
public float TestValue;
public ExampleTable(Guid id, FurnitureType type, int[] colors, Map map, Vector2 pos, float floor) : base(id, type, colors, map, pos, floor) {
this.TestValue = Furniture.Random.NextSingle();
}
}

View file

@ -1,29 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="TinyLifeApi" Version="0.42.4" />
<PackageReference Include="TinyLifeApi" Version="0.27.0" />
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.1.303" />
<PackageReference Include="Lib.Harmony" Version="2.2.2" />
<PackageReference Include="ExtremelySimpleLogger" Version="1.4.1" />
<PackageReference Include="MLEM.Data" Version="6.3.0" />
<PackageReference Include="MLEM.Extended" Version="6.3.0" />
<PackageReference Include="MLEM.Startup" Version="6.3.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="DynamicEnums" Version="1.2.0" />
<PackageReference Include="Coroutine" Version="2.1.5" />
<PackageReference Include="ExtremelySimpleLogger" Version="1.2.5" />
<PackageReference Include="Lib.Harmony" Version="2.2.1" />
<PackageReference Include="MLEM.Data" Version="6.0.0" />
<PackageReference Include="MLEM.Extended" Version="6.0.0" />
<PackageReference Include="MLEM.Startup" Version="6.0.0" />
<PackageReference Include="MonoGame.Extended" Version="3.8.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.1.263" />
</ItemGroup>
<ItemGroup>
<Content Include="./Content/**">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="SteamThumbnail.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

8
ModOptions.cs Normal file
View file

@ -0,0 +1,8 @@
namespace ExampleMod;
// these options are saved and loaded in ExampleMod
public class ModOptions {
public float DarkShirtSpeedIncrease = 2;
}

55
SitDownOnGrassAction.cs Normal file
View file

@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using TinyLife;
using TinyLife.Actions;
using TinyLife.Emotions;
using TinyLife.Objects;
using Action = TinyLife.Actions.Action;
namespace ExampleMod;
// we use a multi action because we want to walk to the location, and then execute the main sitting part
// see CustomTable for information on how to store custom action-specific information to disk as well
public class SitDownOnGrassAction : MultiAction {
public SitDownOnGrassAction(ActionType type, ActionInfo info) : base(type, info) {}
protected override IEnumerable<Action> CreateFirstActions() {
// we want to walk to the location clicked, so we use the current action info
yield return ActionType.GoHere.Construct(this.Info);
// if multiple things should be done before starting this action, they can all be returned here
}
protected override void AndThenInitialize() {
// this is called when the main action starts (after going to the location, in our case)
// but we don't need to do anything here for our action
}
protected override void AndThenUpdate(GameTime time, TimeSpan passedInGame, float speedMultiplier) {
base.AndThenUpdate(time, passedInGame, speedMultiplier);
// this method gets called every update frame while the action is active
// set our person to look like they're sitting on the ground
this.Person.CurrentPose = Person.Pose.SittingGround;
// restore need and lower emotions
this.Person.RestoreNeed(NeedType.Energy, 0.5F, this.Info, speedMultiplier);
this.Person.LowerEmotion(EmotionType.Uncomfortable, 0.0001F, speedMultiplier);
}
protected override CompletionType AndThenIsCompleted() {
// we want to complete our action once 10 minutes of sitting time have passed
return this.CompleteInTime(TimeSpan.FromMinutes(10));
}
protected override void AndThenOnCompleted(CompletionType type) {
base.AndThenOnCompleted(type);
// this method is called when the action completes in any way, even if it fails
if (type == CompletionType.Completed) {
// once we're finished sitting, we want to get a nice emotion modifier for it
this.Person.AddEmotion(ExampleMod.GrassSittingModifier, 2, TimeSpan.FromHours(1), this.Type);
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 279 B

View file

@ -4,42 +4,34 @@ using System.Threading;
var target = Argument("target", "Run");
var config = Argument("configuration", "Release");
var args = Argument("args", "");
var tinyLifeDir = $"{Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)}/Tiny Life";
Task("Clean").Does(() => {
EnsureDirectoryDoesNotExist($"bin/{config}");
EnsureDirectoryDoesNotExist($"{tinyLifeDir}/Mods/_Dev");
});
Task("Build").DoesForEach(GetFiles("**/*.csproj"), p => {
DotNetBuild(p.FullPath, new DotNetBuildSettings {
Configuration = config
});
DeleteFiles($"bin/{config}/**/*");
DotNetBuild(p.FullPath, new DotNetBuildSettings { Configuration = config });
});
Task("CopyToMods").IsDependentOn("Build").Does(() => {
var dir = $"{tinyLifeDir}/Mods/_Dev";
EnsureDirectoryExists(dir);
var dir = $"{tinyLifeDir}/Mods";
CreateDirectory(dir);
var files = GetFiles($"bin/{config}/net*/**/*");
CopyFiles(files, dir, true);
});
Task("Run").IsDependentOn("CopyToMods").Does(() => {
// start the tiny life process
var exeDir = System.IO.File.ReadAllText($"{tinyLifeDir}/GameDir");
var process = Process.Start(new ProcessStartInfo($"{exeDir}/Tiny Life") {
Arguments = $"-v --skip-splash --skip-preloads --debug-saves --ansi {args}",
RedirectStandardOutput = true,
RedirectStandardError = true
var exeDir = $"{tinyLifeDir}/GameDir";
if (!FileExists(exeDir))
throw new Exception("Didn't find game directory information. Run the game manually at least once to allow the Run task to be executed.");
var exe = $"{System.IO.File.ReadAllText(exeDir)}/Tiny Life";
var process = Process.Start(new ProcessStartInfo(exe) {
Arguments = "-v --skip-splash --skip-preloads",
CreateNoWindow = true
});
// make sure the output buffers (which we ignore) don't fill up
process.BeginOutputReadLine();
process.BeginErrorReadLine();
// we wait a bit to make sure the process has generated a new log file
Thread.Sleep(1000);
// we wait a bit to make sure the process has generated a new log file, bleh
Thread.Sleep(3000);
// attach to the newest log file
var logsDir = $"{tinyLifeDir}/Logs";
@ -48,16 +40,16 @@ Task("Run").IsDependentOn("CopyToMods").Does(() => {
using (var stream = new FileStream(log, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) {
using (var reader = new StreamReader(stream)) {
var lastPos = 0L;
do {
if (stream.Length > lastPos) {
stream.Seek(lastPos, SeekOrigin.Begin);
while (!process.HasExited) {
if (reader.BaseStream.Length > lastPos) {
reader.BaseStream.Seek(lastPos, SeekOrigin.Begin);
string line;
while ((line = reader.ReadLine()) != null)
Information(line);
lastPos = stream.Position;
lastPos = reader.BaseStream.Position;
}
Thread.Sleep(10);
} while (!process.HasExited);
}
}
}
}