-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathItemCommandModule.cs
More file actions
245 lines (218 loc) · 12 KB
/
Copy pathItemCommandModule.cs
File metadata and controls
245 lines (218 loc) · 12 KB
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
using Discord;
using Discord.Interactions;
using Microsoft.Extensions.DependencyInjection;
using RustPlusBot.Features.Commands.Formatting;
using RustPlusBot.Features.ItemData;
using RustPlusBot.Features.ItemData.Data;
using RustPlusBot.Features.ItemData.Lookup;
using RustPlusBot.Features.ItemData.Naming;
using RustPlusBot.Localization;
using RustPlusBot.Persistence.Workspace;
namespace RustPlusBot.Features.Commands.Modules;
/// <summary>The /item, /recycle, /craft, /research, /decay, /upkeep, /durability, and /smelt slash commands.</summary>
/// <param name="scopeFactory">Creates a short-lived DI scope per interaction.</param>
public sealed class ItemCommandModule(IServiceScopeFactory scopeFactory)
: InteractionModuleBase<SocketInteractionContext>
{
/// <summary>Looks up an item's name, id, stack size, and despawn time.</summary>
/// <param name="item">The item name or id.</param>
[SlashCommand("item", "Look up an item")]
public Task ItemAsync([Summary("item", "Item name or id")] string item) =>
RespondForAsync(item, db => db.Sources.NamesAsOf, (_, _, rec, loc, culture) =>
loc.Get("command.item.ok", culture, ItemLine.Format(rec)));
/// <summary>Shows recycler output for an item.</summary>
/// <param name="item">The item name or id.</param>
[SlashCommand("recycle", "Show recycler output for an item")]
public Task RecycleAsync([Summary("item", "Item name or id")] string item) =>
RespondForAsync(item, db => db.Sources.RecycleAsOf, (_, names, rec, loc, culture) => rec.Recycle is not null
? loc.Get("command.recycle.ok", culture, RecycleLine.Format(rec, names))
: loc.Get("command.recycle.none", culture, rec.Name));
/// <summary>Shows an item's craft recipe.</summary>
/// <param name="item">The item name or id.</param>
[SlashCommand("craft", "Show an item's craft recipe")]
public Task CraftAsync([Summary("item", "Item name or id")] string item) =>
RespondForAsync(item, db => db.Sources.CraftAsOf, (_, names, rec, loc, culture) => rec.Craft is not null
? loc.Get("command.craft.ok", culture, CraftLine.Format(rec, names))
: loc.Get("command.craft.none", culture, rec.Name));
/// <summary>Shows an item's research scrap cost.</summary>
/// <param name="item">The item name or id.</param>
[SlashCommand("research", "Show an item's research scrap cost")]
public Task ResearchAsync([Summary("item", "Item name or id")] string item) =>
RespondForAsync(item, db => db.Sources.ResearchAsOf, (_, _, rec, loc, culture) => rec.Research is not null
? loc.Get("command.research.ok", culture, ResearchLine.Format(rec))
: loc.Get("command.research.none", culture, rec.Name));
/// <summary>Shows an item's decay time.</summary>
/// <param name="item">The item name or id.</param>
[SlashCommand("decay", "Show an item's decay time")]
public Task DecayAsync([Summary("item", "Item name or id")] string item) =>
RespondForAsync(item, db => db.Sources.DecayAsOf, (_, _, rec, loc, culture) => rec.Decay is not null
? loc.Get("command.decay.ok", culture, DecayLine.Format(rec))
: loc.Get("command.decay.none", culture, rec.Name));
/// <summary>Shows a building block's upkeep cost.</summary>
/// <param name="item">The item name or id.</param>
[SlashCommand("upkeep", "Show a building block's upkeep cost")]
public Task UpkeepAsync([Summary("item", "Item name or id")] string item) =>
RespondForAsync(item, db => db.Sources.UpkeepAsOf, (_, names, rec, loc, culture) => rec.Upkeep is not null
? loc.Get("command.upkeep.ok", culture, UpkeepLine.Format(rec, names))
: loc.Get("command.upkeep.none", culture, rec.Name));
/// <summary>Lists the explosives needed to destroy a target.</summary>
/// <param name="target">The item, building block, or vehicle name.</param>
[SlashCommand("durability", "Show the explosives needed to destroy a target")]
public Task DurabilityAsync([Summary("target", "Item, wall/door, or vehicle name")] string target) =>
RespondForRaidAsync(target);
/// <summary>Shows what a smelter converts and its fuel/time cost.</summary>
/// <param name="smelter">The smelter name or id.</param>
[SlashCommand("smelt", "Show what a smelter converts and its fuel/time cost")]
public Task SmeltAsync([Summary("smelter", "Furnace, Camp Fire, Electric Furnace, …")] string smelter) =>
RespondForSmeltAsync(smelter);
/// <summary>Shows a monument's Computer Station CCTV camera codes.</summary>
/// <param name="monument">The monument to look up.</param>
[SlashCommand("cctv", "Show the CCTV camera codes for a monument")]
public Task CctvAsync(
[Summary("monument", "The monument to look up")]
[Choice("Abandoned Military Base", "Abandoned Military Base")]
[Choice("Airfield", "Airfield")]
[Choice("Bandit Camp", "Bandit Camp")]
[Choice("Dome", "Dome")]
[Choice("Large Oil Rig", "Large Oil Rig")]
[Choice("Missile Silo", "Missile Silo")]
[Choice("Outpost", "Outpost")]
[Choice("Small Oil Rig", "Small Oil Rig")]
[Choice("Underwater Labs", "Underwater Labs")]
[Choice("Cargo Ship", "Cargo Ship")]
[Choice("Ferry Terminal", "Ferry Terminal")]
string monument) => RespondForCctvAsync(monument);
private async Task RespondForAsync(
string query,
Func<IItemDatabase, DateOnly> dateSelector,
Func<IItemDatabase, IItemNameResolver, ItemRecord, ILocalizer, string, string> onFound)
{
if (Context.Guild is null)
{
await RespondAsync("This command must be used in a server.", ephemeral: true).ConfigureAwait(false);
return;
}
var scope = scopeFactory.CreateAsyncScope();
await using (scope.ConfigureAwait(false))
{
var db = scope.ServiceProvider.GetRequiredService<IItemDatabase>();
var names = scope.ServiceProvider.GetRequiredService<IItemNameResolver>();
var loc = scope.ServiceProvider.GetRequiredService<ILocalizer>();
var workspace = scope.ServiceProvider.GetRequiredService<IWorkspaceStore>();
var culture = await workspace.GetCultureAsync(Context.Guild.Id).ConfigureAwait(false);
var text = db.Resolve(query) switch
{
ItemMatch.Found f => onFound(db, names, f.Item, loc, culture),
ItemMatch.Ambiguous a => loc.Get("command.item.ambiguous", culture,
string.Join(", ", a.Candidates.Select(c => c.Name))),
_ => loc.Get("command.item.notfound", culture, query),
};
var embed = new EmbedBuilder()
.WithDescription(text)
.WithFooter($"data as of {dateSelector(db):yyyy-MM-dd}")
.Build();
await RespondAsync(ephemeral: true, embed: embed).ConfigureAwait(false);
}
}
private async Task RespondForRaidAsync(string query)
{
if (Context.Guild is null)
{
await RespondAsync("This command must be used in a server.", ephemeral: true).ConfigureAwait(false);
return;
}
var scope = scopeFactory.CreateAsyncScope();
await using (scope.ConfigureAwait(false))
{
var db = scope.ServiceProvider.GetRequiredService<IItemDatabase>();
var names = scope.ServiceProvider.GetRequiredService<IItemNameResolver>();
var loc = scope.ServiceProvider.GetRequiredService<ILocalizer>();
var workspace = scope.ServiceProvider.GetRequiredService<IWorkspaceStore>();
var culture = await workspace.GetCultureAsync(Context.Guild.Id).ConfigureAwait(false);
var text = db.ResolveRaidTarget(query) switch
{
RaidMatch.Found f => loc.Get("command.durability.ok", culture, DurabilityLine.Format(f.Target, names)),
RaidMatch.Ambiguous a => loc.Get("command.item.ambiguous", culture,
string.Join(", ", a.Candidates.Select(c => c.Name))),
_ => loc.Get("command.item.notfound", culture, query),
};
var embed = new EmbedBuilder()
.WithDescription(text)
.WithFooter($"data as of {db.Sources.DurabilityAsOf:yyyy-MM-dd}")
.Build();
await RespondAsync(ephemeral: true, embed: embed).ConfigureAwait(false);
}
}
private async Task RespondForSmeltAsync(string query)
{
if (Context.Guild is null)
{
await RespondAsync("This command must be used in a server.", ephemeral: true).ConfigureAwait(false);
return;
}
var scope = scopeFactory.CreateAsyncScope();
await using (scope.ConfigureAwait(false))
{
var db = scope.ServiceProvider.GetRequiredService<IItemDatabase>();
var names = scope.ServiceProvider.GetRequiredService<IItemNameResolver>();
var loc = scope.ServiceProvider.GetRequiredService<ILocalizer>();
var workspace = scope.ServiceProvider.GetRequiredService<IWorkspaceStore>();
var culture = await workspace.GetCultureAsync(Context.Guild.Id).ConfigureAwait(false);
var text = db.ResolveSmelter(query) switch
{
SmeltMatch.Found f => loc.Get("command.smelt.ok", culture, SmeltLine.Format(f.Smelter, names)),
SmeltMatch.Ambiguous a => loc.Get("command.item.ambiguous", culture,
string.Join(", ", a.Candidates.Select(c => c.Name))),
_ => loc.Get("command.item.notfound", culture, query),
};
var embed = new EmbedBuilder()
.WithDescription(text)
.WithFooter($"data as of {db.Sources.SmeltingAsOf:yyyy-MM-dd}")
.Build();
await RespondAsync(ephemeral: true, embed: embed).ConfigureAwait(false);
}
}
private async Task RespondForCctvAsync(string query)
{
if (Context.Guild is null)
{
await RespondAsync("This command must be used in a server.", ephemeral: true).ConfigureAwait(false);
return;
}
var scope = scopeFactory.CreateAsyncScope();
await using (scope.ConfigureAwait(false))
{
var db = scope.ServiceProvider.GetRequiredService<IItemDatabase>();
var loc = scope.ServiceProvider.GetRequiredService<ILocalizer>();
var workspace = scope.ServiceProvider.GetRequiredService<IWorkspaceStore>();
var culture = await workspace.GetCultureAsync(Context.Guild.Id).ConfigureAwait(false);
var text = db.ResolveCctv(query) switch
{
CctvMatch.Found f => RenderEmbed(f.Monument, loc, culture),
CctvMatch.Ambiguous a => loc.Get("command.item.ambiguous", culture,
string.Join(", ", a.Candidates.Select(c => c.Name))),
_ => loc.Get("command.item.notfound", culture, query),
};
var embed = new EmbedBuilder()
.WithDescription(text)
.WithFooter($"data as of {db.Sources.CctvAsOf:yyyy-MM-dd}")
.Build();
await RespondAsync(ephemeral: true, embed: embed).ConfigureAwait(false);
}
}
/// <summary>
/// Discord-only presentation: fence the codes so wildcard asterisks render literally and the
/// codes are copy-clean; the localized wildcard note rides outside the fence as prose.
/// </summary>
/// <param name="monument">The resolved CCTV monument.</param>
/// <param name="loc">The localizer for string resources.</param>
/// <param name="culture">The guild culture code.</param>
private static string RenderEmbed(CctvMonument monument, ILocalizer loc, string culture)
{
var fenced = loc.Get("command.cctv.ok", culture,
$"```\n{CctvLine.Format(monument)}\n```");
return monument.Dynamic
? $"{fenced}\n\n{loc.Get("command.cctv.note", culture)}"
: fenced;
}
}