forked from Pathoschild/StardewMods
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathModEntry.cs
333 lines (290 loc) · 14.2 KB
/
ModEntry.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
using System;
using System.IO;
using System.Linq;
using Harmony;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Pathoschild.Stardew.Common;
using Pathoschild.Stardew.SmallBeachFarm.Framework;
using Pathoschild.Stardew.SmallBeachFarm.Framework.Config;
using StardewModdingAPI;
using StardewModdingAPI.Events;
using StardewValley;
using StardewValley.Objects;
using xTile;
using xTile.Dimensions;
using xTile.Tiles;
using Rectangle = Microsoft.Xna.Framework.Rectangle;
namespace Pathoschild.Stardew.SmallBeachFarm
{
/// <summary>The mod entry class loaded by SMAPI.</summary>
public class ModEntry : Mod, IAssetLoader
{
/*********
** Fields
*********/
/// <summary>The MD5 hash for the default data.json file.</summary>
private const string DataFileHash = "641585fd329fac69e377cb911cf70862";
/// <summary>The pixel position at which to place the player after they arrive from Marnie's ranch.</summary>
private readonly Vector2 MarnieWarpArrivalPixelPos = new Vector2(76, 21) * Game1.tileSize;
/// <summary>The relative path to the folder containing tilesheet variants.</summary>
private readonly string TilesheetsPath = Path.Combine("assets", "tilesheets");
/// <summary>The relative path to the folder containing tilesheet overlays.</summary>
private readonly string OverlaysPath = Path.Combine("assets", "overlays");
/// <summary>The asset name for the map to replace.</summary>
private string FarmMapAssetName;
/// <summary>The mod configuration.</summary>
private ModConfig Config;
/// <summary>The mod's hardcoded data.</summary>
private ModData Data;
/// <summary>The minimum value to consider non-transparent.</summary>
/// <remarks>On Linux/Mac, fully transparent pixels may have an alpha up to 4 for some reason.</remarks>
private const byte MinOpacity = 5;
/// <summary>A fake asset key prefix from which to load tilesheets.</summary>
private string FakeAssetPrefix => Path.Combine("Mods", this.ModManifest.UniqueID);
/*********
** Public methods
*********/
/// <summary>The mod entry point, called after the mod is first loaded.</summary>
/// <param name="helper">Provides simplified APIs for writing mods.</param>
public override void Entry(IModHelper helper)
{
// read data
this.Data = this.Helper.Data.ReadJsonFile<ModData>("assets/data.json");
{
string dataPath = Path.Combine(this.Helper.DirectoryPath, "assets", "data.json");
if (this.Data == null || !File.Exists(dataPath))
{
this.Monitor.Log("The mod's 'assets/data.json' file is missing, so this mod can't work correctly. Please reinstall the mod to fix this.", LogLevel.Error);
return;
}
if (CommonHelper.GetFileHash(dataPath) != ModEntry.DataFileHash)
this.Monitor.Log("Found edits to 'assets/data.json'.");
}
// read config
this.Config = this.Helper.ReadConfig<ModConfig>();
this.FarmMapAssetName = this.Data.FarmMaps.FirstOrDefault(p => p.ID == this.Config.ReplaceFarmID)?.Map;
if (this.FarmMapAssetName == null)
{
this.Monitor.Log("You have an invalid farm ID in the 'config.json' file. You can delete the file to reset it. This mod will be disabled.", LogLevel.Error);
return;
}
// hook events
helper.Events.Player.Warped += this.OnWarped;
helper.Events.GameLoop.DayEnding += this.DayEnding;
helper.Events.GameLoop.GameLaunched += this.OnGameLaunched;
// hook Harmony patch
var harmony = HarmonyInstance.Create(this.ModManifest.UniqueID);
FarmPatcher.Hook(
harmony,
this.Monitor,
addCampfire: this.Config.AddCampfire,
useBeachMusic: this.Config.UseBeachMusic,
isSmallBeachFarm: location => this.IsSmallBeachFarm(location, out _),
getFishType: this.GetFishType
);
}
/// <summary>Get whether this instance can load the initial version of the given asset.</summary>
/// <param name="asset">Basic metadata about the asset being loaded.</param>
public bool CanLoad<T>(IAssetInfo asset)
{
if (this.FarmMapAssetName == null)
return false;
return
asset.AssetNameEquals(this.FarmMapAssetName)
|| asset.AssetName.StartsWith(this.FakeAssetPrefix);
}
/// <summary>Load a matched asset.</summary>
/// <param name="asset">Basic metadata about the asset being loaded.</param>
public T Load<T>(IAssetInfo asset)
{
// load map
if (asset.AssetNameEquals(this.FarmMapAssetName))
{
// load map
Map map = this.Helper.Content.Load<Map>("assets/farm.tmx");
// add islands
if (this.Config.EnableIslands)
{
Map islands = this.Helper.Content.Load<Map>("assets/islands.tmx");
this.Helper.Content.GetPatchHelper(map)
.AsMap()
.PatchMap(source: islands, targetArea: new Rectangle(0, 26, 56, 49));
}
// add campfire
if (this.Config.AddCampfire)
{
var buildingsLayer = map.GetLayer("Buildings");
buildingsLayer.Tiles[65, 23] = new StaticTile(buildingsLayer, map.GetTileSheet("zbeach"), BlendMode.Alpha, 157); // driftwood pile
buildingsLayer.Tiles[64, 22] = new StaticTile(buildingsLayer, map.GetTileSheet("untitled tile sheet"), BlendMode.Alpha, 242); // campfire
}
// apply tilesheet recolors
string internalRootKey = this.Helper.Content.GetActualAssetKey(Path.Combine(this.TilesheetsPath, "_default"));
foreach (TileSheet tilesheet in map.TileSheets)
{
if (tilesheet.ImageSource.StartsWith(internalRootKey + Path.DirectorySeparatorChar))
tilesheet.ImageSource = this.Helper.Content.GetActualAssetKey(Path.Combine(this.FakeAssetPrefix, Path.GetFileNameWithoutExtension(tilesheet.ImageSource)), ContentSource.GameContent);
}
return (T)(object)map;
}
// load tilesheet
if (asset.AssetName.StartsWith(this.FakeAssetPrefix))
{
string filename = Path.GetFileName(asset.AssetName);
if (!Path.HasExtension(filename))
filename += ".png";
// get relative path to load
string relativePath = new DirectoryInfo(this.GetFullPath(this.TilesheetsPath))
.EnumerateDirectories()
.FirstOrDefault(p => p.Name != "_default" && this.Helper.ModRegistry.IsLoaded(p.Name))
?.Name;
relativePath = Path.Combine(this.TilesheetsPath, relativePath ?? "_default", filename);
// load asset
Texture2D tilesheet = this.Helper.Content.Load<Texture2D>(relativePath);
var tilesheetPixels = new Lazy<Color[]>(() => this.GetPixels(tilesheet));
// apply overlays
foreach (DirectoryInfo folder in new DirectoryInfo(this.GetFullPath(this.OverlaysPath)).EnumerateDirectories())
{
if (!this.Helper.ModRegistry.IsLoaded(folder.Name))
continue;
// get overlay
Texture2D overlay = this.Helper.Content.Load<Texture2D>(Path.Combine(this.OverlaysPath, folder.Name, filename));
Color[] overlayPixels = this.GetPixels(overlay);
// apply
Color[] target = tilesheetPixels.Value;
for (int i = 0; i < overlayPixels.Length; i++)
{
Color pixel = overlayPixels[i];
if (pixel.A >= ModEntry.MinOpacity)
target[i] = overlayPixels[i];
}
}
if (tilesheetPixels.IsValueCreated)
tilesheet.SetData(tilesheetPixels.Value);
return (T)(object)tilesheet;
}
// unknown asset
throw new NotSupportedException($"Unexpected asset '{asset.AssetName}'.");
}
/*********
** Private methods
*********/
/// <summary>The event called after the first game update, once all mods are loaded.</summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event data.</param>
private void OnGameLaunched(object sender, GameLaunchedEventArgs e)
{
// add Generic Mod Config Menu integration
new GenericModConfigMenuIntegrationForSmallBeachFarm(
getConfig: () => this.Config,
modData: this.Data,
reset: () =>
{
this.Config = new ModConfig();
this.Helper.WriteConfig(this.Config);
},
saveAndApply: () => this.Helper.WriteConfig(this.Config),
modRegistry: this.Helper.ModRegistry,
monitor: this.Monitor,
manifest: this.ModManifest
).Register();
}
/// <summary>Raised after a player warps to a new location.</summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event data.</param>
private void OnWarped(object sender, WarpedEventArgs e)
{
// move player if they warp into the ocean (e.g. from Marnie's ranch)
// note: getTileLocation() seems to be unreliable when mounted.
if (e.IsLocalPlayer && this.IsSmallBeachFarm(e.NewLocation, out Farm farm))
{
Vector2 tile = e.Player.Position / Game1.tileSize;
if (this.IsInvalidPosition(farm, (int)tile.X, (int)tile.Y))
Game1.player.Position = this.MarnieWarpArrivalPixelPos;
}
}
/// <summary>Raised before the game ends the current day.</summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event data.</param>
private void DayEnding(object sender, DayEndingEventArgs e)
{
if (!this.IsSmallBeachFarm(Game1.getFarm(), out Farm farm))
return;
// update ocean crabpots before the game does
GameLocation beach = Game1.getLocationFromName("Beach");
foreach (CrabPot pot in farm.objects.Values.OfType<CrabPot>())
{
if (this.GetFishType(farm, (int)pot.TileLocation.X, (int)pot.TileLocation.Y) == FishType.Ocean)
pot.DayUpdate(beach);
}
}
/// <summary>Get the full path for a relative path.</summary>
/// <param name="relative">The relative path.</param>
private string GetFullPath(string relative)
{
return Path.Combine(this.Helper.DirectoryPath, relative);
}
/// <summary>Get the pixel data for a texture.</summary>
/// <param name="texture">The texture asset.</param>
private Color[] GetPixels(Texture2D texture)
{
Color[] pixels = new Color[texture.Width * texture.Height];
texture.GetData(pixels);
return pixels;
}
/// <summary>Get whether the given location is the Small Beach Farm.</summary>
/// <param name="location">The location to check.</param>
/// <param name="farm">The farm instance.</param>
private bool IsSmallBeachFarm(GameLocation location, out Farm farm)
{
if (Game1.whichFarm == this.Config.ReplaceFarmID && location is Farm farmInstance && farmInstance.Name == "Farm")
{
farm = farmInstance;
return true;
}
farm = null;
return false;
}
/// <summary>Get the fish that should be available from the given tile.</summary>
/// <param name="farm">The farm instance.</param>
/// <param name="x">The tile X position.</param>
/// <param name="y">The tile Y position.</param>
private FishType GetFishType(Farm farm, int x, int y)
{
// not water
// This should never happen since it's only called when catching a fish, but just in
// case fallback to the default farm logic.
if (farm.doesTileHaveProperty(x, y, "Water", "Back") == null)
return FishType.River;
// mixed fish area
if (this.Data.MixedFishAreas.Any(p => p.Contains(x, y)))
{
return Game1.random.Next(2) == 1
? FishType.Ocean
: FishType.River;
}
// ocean or river
string tilesheetId = farm.map
?.GetLayer("Back")
?.PickTile(new Location(x * Game1.tileSize, y * Game1.tileSize), Game1.viewport.Size)
?.TileSheet
?.Id;
return tilesheetId == "zbeach" || tilesheetId == "zbeach_farm"
? FishType.Ocean
: FishType.River;
}
/// <summary>Get whether the player shouldn't be able to access a given position.</summary>
/// <param name="farm">The farm instance to check.</param>
/// <param name="x">The tile X position.</param>
/// <param name="y">The tile Y position.</param>
private bool IsInvalidPosition(Farm farm, int x, int y)
{
return
farm.doesTileHaveProperty(x, y, "Water", "Back") != null
|| (
!farm.isTilePassable(new Location(x, y), Game1.viewport)
&& farm.doesTileHaveProperty(x, y, "Passable", "Buildings") != null
);
}
}
}