-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathScriptedHand.cs
332 lines (285 loc) · 11.8 KB
/
ScriptedHand.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
using System;
using System.IO;
using MoonSharp.Interpreter;
using StardewModdingAPI;
using StardewValley;
using StardewModdingAPI.Events;
using System.Linq;
using System.Collections;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.ComponentModel;
namespace ScriptedHand
{
/// <summary>The mod entry point.</summary>
internal sealed class ModEntry : Mod
{
/*********
** 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)
{
this.LoadConsoleCommands(helper);
helper.Events.Input.ButtonPressed += OnButtonPressed;
Script.DefaultOptions.DebugPrint = (str) =>
{
Monitor.Log(str, LogLevel.Info);
};
}
void OnButtonPressed(object sender, ButtonPressedEventArgs ev)
{
//if (!Context.IsWorldReady) return;
if (Game1.activeClickableMenu != null || (!Context.IsPlayerFree))
return;
// Display UI if user presses J
if (ev.Button == SButton.J)
Game1.activeClickableMenu = new ControlPanel();
}
/*********
** Private methods
*********/
private void RunScript(string command, string[] args)
{
string s = File.ReadAllText(Path.Combine(Helper.DirectoryPath,
"Lua", args[0]));
if (s.Split(Environment.NewLine)[0] != "function main()")
{
// requires main function so the script can't do anything funky
Monitor.Log($"Lua Error: Script \"{args[0]}\" does not start " +
$"with a main function!", LogLevel.Error);
return;
}
Script script = new();
InjectAPI(script);
script.DoString(s);
script.Call(script.Globals["main"]); // call main function
}
private void RunScripts(string command, string[] args)
{
foreach (string scriptName in args)
{
Script.RunString(File.ReadAllText
(Path.Combine(this.Helper.DirectoryPath,
"Lua", scriptName)));
}
}
/// <summary>Method to load all the SMAPI console commands.
/// For organization purposes.</summary>
private void LoadConsoleCommands(IModHelper helper)
{
helper.ConsoleCommands.Add(
"run_script",
@"Run the specified Lua script.
Example: run_script helloworld.lua",
RunScript
);
helper.ConsoleCommands.Add(
"run_scripts",
@"Run a bunch of scripts in order from left to right.
Example: run_scripts script1.lua script2.lua script3.lua",
RunScripts
);
LoadEntryScript(helper);
}
private void LoadEntryScript(IModHelper helper) {
// Load console commands from Lua files
string p = Path.Combine(Helper.DirectoryPath, "Lua", "_entry.lua");
if (File.Exists(p))
{
Monitor.LogOnce("Detected _entry.lua, running script...");
string s = File.ReadAllText(p);
if (!s.Contains("function commands()"))
{
Monitor.Log($"Lua Error: _entry.lua does not contain a " +
$"commands function, and was prevented from running.",
LogLevel.Error);
return;
}
Script script = new();
InjectAPI(script);
script.DoString(s);
DynValue commands = script.Globals.Get("commands");
Table commandTable = commands.Table;
// Add console commands from script
foreach (TablePair pair in commandTable.Pairs)
{
Table table = pair.Value.Table;
helper.ConsoleCommands.Add(
table.Get(1).String,
table.Get(2).String,
(command, args) =>
{
script.Call(script.Globals[table.Get(3).String],
command, args);
}
);
}
// check for code that could crash SDV
if (CheckScript(s))
throw new Exception("WARNING: Your script could cause the game to hang and/or crash, so it was prevented from running. You can try removing any \"while true\" loops to let it run.");
// Run the startup script's main() as well
if (TableContains(script.Globals, "main"))
script.Call(script.Globals.Get("main"));
// and finally, load the controls
// (the most complicated part)
Table controlsTable = script.Globals.Get("controls").Table;
string layoutType = controlsTable.Get("layout").String;
Table controls = controlsTable.Get("controls").Table;
List<ControlData> controlsData = new List<ControlData>();
foreach(TablePair pair in controls.Pairs)
{
Table control = pair.Value.Table;
string controlName = control.Get(1).String;
string controlType = control.Get(2).String;
ControlPanel.ControlType eControlType;
Table controlProperties = control.Get(3).Table;
string controlValue = controlType != "BUTTON" ? control.Get(4).String : "";
switch (controlType)
{
case "BUTTON":
eControlType = ControlPanel.ControlType.Button; break;
case "CHECKBOX":
eControlType = ControlPanel.ControlType.Checkbox; break;
case "NUMBERINPUT":
eControlType = ControlPanel.ControlType.NumberInput; break;
case "TEXTINPUT":
eControlType = ControlPanel.ControlType.TextInput; break;
case "SLIDER":
eControlType = ControlPanel.ControlType.Slider; break;
default:
throw new Exception($"Lua Error: {controlType} is not a valid control type!");
}
if (controlType == "BUTTON")
{
controlsData.Add(new ControlData(controlName, controlProperties.Get("text").String, eControlType));
continue;
}
}
}
}
/// <summary>
/// Checks if a Table contains a certain key.
/// </summary>
/// <param name="table">The Table to check.</param>
/// <param name="key">The key to look for.</param>
/// <returns>true if the key was found.</returns>
internal bool TableContains(Table table, string key)
{
return !(table.Keys.All((DynValue d) => d.String != key));
}
/// <summary>
/// Checks a script for malicious code.
/// </summary>
/// <param name="script">The script, as a string, to check.</param>
/// <returns>true if malicious code was found.</returns>
internal bool CheckScript(string script)
{
return script.Contains("while true do");
}
/// <summary>
/// Called to let Lua use C# methods.
/// </summary>
/// <param name="script">The Script object to allow access to.</param>
internal void InjectAPI(Script script)
{
//script.Globals["movePlayer"] = (Func<int, int, int>)MovePlayer;
//script.Globals["interact"] = (Func<int>)Interact;
script.Globals["printl"] = (Action<string, string>)SMAPIPrint;
// controlPanel.* api
Table controlPanel = new Table(script);
// This was the only way I could think of to convert AddControl
// to a DynValue, and I'm sorry.
// (I could have made it a one-liner which would have been worse)
//Action<string, string, ControlPanel.ControlType> acDelegate
// = ControlPanel.AddControl;
//DynValue acdelToDynValue = DynValue.FromObject(script, acDelegate);
//Monitor.Log($"{acdelToDynValue.Type}", LogLevel.Debug);
//controlPanel["addControl"] = acdelToDynValue;
}
/*****************
* Scripting API *
*****************/
// TODO: harmony hijacking the InputState.GetGamePadState and pretending to be a game controller
/// <summary>
/// A type representing either the Left or Right mouse button.
/// </summary>
private enum MouseButton { Left, Right };
/// <summary>
/// Moves the player by (x, y) tiles horizontally
/// and/or vertically.
/// </summary>
/// <param name="x">Amount of tiles to move
/// horizontally.</param>
/// <param name="y">Amount of tiles to move
/// vertically.</param>
private void MovePlayer(int x, int y)
{
//PathFindController pfc = new(Game1.player, );
//pfc.;
// TODO: figure out how to move player x & y
}
/// <summary>
/// Emulates interacting with a tile in front
/// of the player.
/// </summary>
private void Interact()
{
// TODO: force player to interact
}
/// <summary>
/// Emulates using a tool on a tile in front
/// of the player. Depends on the hotbar slot
/// currently selected.
/// </summary>
private void UseTool()
{
// TODO: force player to use tool
}
/// <summary>
/// Switches the currently active hotbar slot to the one
/// specified.
/// </summary>
/// <param name="slot">The hotbar slot to switch to,
/// from 0-9 as well as - and =.</param>
private void SwitchItemTo(int slot)
{
// TODO: figure out how to emulate keyboard input or switch hotbar slots from C#
}
/// <summary>
/// Prints to the SMAPI console.
/// </summary>
/// <param name="text">String to print to the console.</param>
/// <param name="logLevel">Log level. Must be one of TRACE | DEBUG | INFO | ALERT | WARN | ERROR.</param>
private void SMAPIPrint(string text, string logLevel)
{
LogLevel ll;
switch(logLevel)
{
case "TRACE":
ll = LogLevel.Trace;
break;
case "DEBUG":
ll = LogLevel.Debug;
break;
case "INFO":
ll = LogLevel.Info;
break;
case "ALERT":
ll = LogLevel.Alert;
break;
case "WARN":
ll = LogLevel.Warn;
break;
case "ERROR":
ll = LogLevel.Error;
break;
default:
this.Monitor.Log("Lua Error: Tried to log with invalid log level!", LogLevel.Error);
return;
}
this.Monitor.Log(text, ll);
}
}
}