Skip to content

Commit 923e83d

Browse files
committed
feat(itemdata): add Decay + Upkeep schema fields (6b)
1 parent 6270b4e commit 923e83d

7 files changed

Lines changed: 79 additions & 27 deletions

File tree

src/RustPlusBot.Features.ItemData/Data/ItemDataset.cs

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,28 +11,36 @@ public sealed record ItemDataset(int SchemaVersion, DatasetSources Sources, IRea
1111
/// <param name="RecycleAsOf">Recycle data source date.</param>
1212
/// <param name="CraftAsOf">Craft data source date.</param>
1313
/// <param name="ResearchAsOf">Research data source date.</param>
14+
/// <param name="DecayAsOf">Decay data source date.</param>
15+
/// <param name="UpkeepAsOf">Upkeep data source date.</param>
1416
public sealed record DatasetSources(
1517
DateOnly NamesAsOf,
1618
DateOnly RecycleAsOf,
1719
DateOnly CraftAsOf,
18-
DateOnly ResearchAsOf);
20+
DateOnly ResearchAsOf,
21+
DateOnly DecayAsOf,
22+
DateOnly UpkeepAsOf);
1923

20-
/// <summary>One item, with all 6a calculator data inlined (null where not applicable).</summary>
24+
/// <summary>One item, with all calculator data inlined (null where not applicable).</summary>
2125
/// <param name="Id">The Rust item id.</param>
2226
/// <param name="Name">The display name.</param>
2327
/// <param name="StackSize">Max stack size.</param>
2428
/// <param name="DespawnSeconds">Despawn time in seconds, or null if it does not despawn / unknown.</param>
2529
/// <param name="Recycle">Recycler yield, or null if not recyclable.</param>
2630
/// <param name="Craft">Craft recipe, or null if not craftable.</param>
2731
/// <param name="Research">Research cost, or null if not researchable.</param>
32+
/// <param name="Decay">Decay timing, or null if the item does not decay / unknown.</param>
33+
/// <param name="Upkeep">Upkeep cost, or null if the item has no upkeep.</param>
2834
public sealed record ItemRecord(
2935
int Id,
3036
string Name,
3137
int StackSize,
3238
int? DespawnSeconds,
3339
RecycleYield? Recycle,
3440
CraftRecipe? Craft,
35-
ResearchCost? Research);
41+
ResearchCost? Research,
42+
DecayInfo? Decay,
43+
UpkeepCost? Upkeep);
3644

3745
/// <summary>Recycler output for an item (6a covers the standard recycler only).</summary>
3846
/// <param name="Recycler">The yield entries produced by the standard recycler.</param>
@@ -58,3 +66,28 @@ public sealed record Ingredient(int ItemId, int Quantity);
5866
/// <summary>The research cost for an item.</summary>
5967
/// <param name="Scrap">The scrap cost to research.</param>
6068
public sealed record ResearchCost(int Scrap);
69+
70+
/// <summary>Decay timing for an item/deployable/building block. All fields nullable — a field is
71+
/// populated only when RustLabs provides it. <paramref name="Seconds"/> is the base/default decay.</summary>
72+
/// <param name="Seconds">Base decay time in seconds.</param>
73+
/// <param name="OutsideSeconds">Decay time when placed outside, if distinct.</param>
74+
/// <param name="InsideSeconds">Decay time when placed inside, if distinct.</param>
75+
/// <param name="UnderwaterSeconds">Decay time when underwater, if distinct.</param>
76+
/// <param name="Hp">The item's hit points.</param>
77+
public sealed record DecayInfo(
78+
int? Seconds,
79+
int? OutsideSeconds,
80+
int? InsideSeconds,
81+
int? UnderwaterSeconds,
82+
int? Hp);
83+
84+
/// <summary>The upkeep cost to maintain a building block.</summary>
85+
/// <param name="Entries">The per-resource upkeep cost entries.</param>
86+
public sealed record UpkeepCost(IReadOnlyList<UpkeepEntry> Entries);
87+
88+
/// <summary>One upkeep resource cost. <paramref name="QuantityMin"/> equals
89+
/// <paramref name="QuantityMax"/> for a single (non-range) quantity.</summary>
90+
/// <param name="ItemId">The resource item id.</param>
91+
/// <param name="QuantityMin">The lower bound of the cost.</param>
92+
/// <param name="QuantityMax">The upper bound of the cost.</param>
93+
public sealed record UpkeepEntry(int ItemId, int QuantityMin, int QuantityMax);

tests/RustPlusBot.Features.Commands.Tests/Formatting/ItemFormatterTests.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public sealed class ItemFormatterTests
1212
[Fact]
1313
public void ItemLine_IncludesNameAndStack()
1414
{
15-
var item = new ItemRecord(1, "AK-47", 1, 3600, null, null, null);
15+
var item = new ItemRecord(1, "AK-47", 1, 3600, null, null, null, null, null);
1616
var line = ItemLine.Format(item);
1717
Assert.Contains("AK-47", line, StringComparison.Ordinal);
1818
Assert.Contains("1", line, StringComparison.Ordinal);
@@ -22,7 +22,7 @@ public void ItemLine_IncludesNameAndStack()
2222
public void RecycleLine_ListsYields()
2323
{
2424
var item = new ItemRecord(1, "AK-47", 1, null,
25-
new RecycleYield([new YieldEntry(-1059362949, 4, 1.0)]), null, null);
25+
new RecycleYield([new YieldEntry(-1059362949, 4, 1.0)]), null, null, null, null);
2626
var line = RecycleLine.Format(item, _names);
2727
Assert.Contains("AK-47", line, StringComparison.Ordinal);
2828
Assert.Contains("4", line, StringComparison.Ordinal);
@@ -32,15 +32,15 @@ public void RecycleLine_ListsYields()
3232
public void CraftLine_ListsIngredients()
3333
{
3434
var item = new ItemRecord(1, "AK-47", 1, null, null,
35-
new CraftRecipe([new Ingredient(-1059362949, 200)], 30.0, 3), null);
35+
new CraftRecipe([new Ingredient(-1059362949, 200)], 30.0, 3), null, null, null);
3636
var line = CraftLine.Format(item, _names);
3737
Assert.Contains("200", line, StringComparison.Ordinal);
3838
}
3939

4040
[Fact]
4141
public void ResearchLine_ShowsScrap()
4242
{
43-
var item = new ItemRecord(1, "AK-47", 1, null, null, null, new ResearchCost(500));
43+
var item = new ItemRecord(1, "AK-47", 1, null, null, null, new ResearchCost(500), null, null);
4444
var line = ResearchLine.Format(item);
4545
Assert.Contains("500", line, StringComparison.Ordinal);
4646
}

tests/RustPlusBot.Features.ItemData.Tests/EmbeddedItemDatabaseTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ public void Parse_SucceedsOnMatchingSchemaVersion()
5050
[Fact]
5151
public void IndexById_DuplicateId_LastWins()
5252
{
53-
var first = new ItemRecord(42, "First", 1, null, null, null, null);
54-
var last = new ItemRecord(42, "Last", 1, null, null, null, null);
53+
var first = new ItemRecord(42, "First", 1, null, null, null, null, null, null);
54+
var last = new ItemRecord(42, "Last", 1, null, null, null, null, null, null);
5555
var index = EmbeddedItemDatabase.IndexById([first, last]);
5656
Assert.Equal("Last", index[42].Name);
5757
}

tests/RustPlusBot.Features.ItemData.Tests/ItemDatasetTests.cs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,25 @@ public sealed class ItemDatasetTests
77
[Fact]
88
public void ItemRecord_AllowsNullCalculatorData()
99
{
10-
var record = new ItemRecord(1, "Wood", 1000, null, null, null, null);
10+
var record = new ItemRecord(1, "Wood", 1000, null, null, null, null, null, null);
1111
Assert.Equal("Wood", record.Name);
1212
Assert.Null(record.Recycle);
13+
Assert.Null(record.Decay);
14+
Assert.Null(record.Upkeep);
15+
}
16+
17+
[Fact]
18+
public void ItemRecord_CarriesDecayAndUpkeep()
19+
{
20+
var record = new ItemRecord(
21+
2, "Wooden Door", 1, null, null, null, null,
22+
new DecayInfo(900, null, null, null, 100),
23+
new UpkeepCost([new UpkeepEntry(-151838493, 8, 25)]));
24+
25+
Assert.Equal(900, record.Decay!.Seconds);
26+
Assert.Equal(100, record.Decay.Hp);
27+
Assert.Equal(-151838493, record.Upkeep!.Entries[0].ItemId);
28+
Assert.Equal(8, record.Upkeep.Entries[0].QuantityMin);
29+
Assert.Equal(25, record.Upkeep.Entries[0].QuantityMax);
1330
}
1431
}

tests/RustPlusBot.Features.ItemData.Tests/ItemLookupTests.cs

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ namespace RustPlusBot.Features.ItemData.Tests;
55

66
public sealed class ItemLookupTests
77
{
8-
private static readonly ItemRecord Ak = new(1, "AK-47", 1, null, null, null, null);
9-
private static readonly ItemRecord Semi = new(2, "Semi-Automatic Rifle", 1, null, null, null, null);
10-
private static readonly ItemRecord Wood = new(3, "Wood", 1000, null, null, null, null);
11-
private static readonly ItemRecord Bag = new(4, "Sleeping Bag", 1, null, null, null, null);
8+
private static readonly ItemRecord Ak = new(1, "AK-47", 1, null, null, null, null, null, null);
9+
private static readonly ItemRecord Semi = new(2, "Semi-Automatic Rifle", 1, null, null, null, null, null, null);
10+
private static readonly ItemRecord Wood = new(3, "Wood", 1000, null, null, null, null, null, null);
11+
private static readonly ItemRecord Bag = new(4, "Sleeping Bag", 1, null, null, null, null, null, null);
1212
private static readonly IReadOnlyList<ItemRecord> All = [Ak, Semi, Wood, Bag];
1313

1414
private static ItemMatch Resolve(string q) =>
@@ -64,8 +64,8 @@ public void MultipleSubstring_AmbiguousPrefixFirst()
6464
public void ExactNameCollision_ReturnsAmbiguousOrderedById()
6565
{
6666
// Two items share the same case-insensitive name; the one with the lower id should be first.
67-
var recyclable = new ItemRecord(10, "Sunglasses", 1, null, new RecycleYield([]), null, null);
68-
var notRecyclable = new ItemRecord(5, "Sunglasses", 1, null, null, null, null);
67+
var recyclable = new ItemRecord(10, "Sunglasses", 1, null, new RecycleYield([]), null, null, null, null);
68+
var notRecyclable = new ItemRecord(5, "Sunglasses", 1, null, null, null, null, null, null);
6969
IReadOnlyList<ItemRecord> items = [recyclable, notRecyclable];
7070
var match = ItemLookup.Resolve("sunglasses", id => items.FirstOrDefault(i => i.Id == id), items);
7171
var amb = Assert.IsType<ItemMatch.Ambiguous>(match);
@@ -79,9 +79,9 @@ public void ExactNameCollision_ReturnsAmbiguousOrderedById()
7979
public void ExactNameCollision_DoesNotFallThroughToSubstring()
8080
{
8181
// Even though there are 2 exact matches, the result must be Ambiguous (not a substring-ranked list).
82-
var a = new ItemRecord(1, "Sunglasses", 1, null, null, null, null);
83-
var b = new ItemRecord(2, "Sunglasses", 1, null, null, null, null);
84-
var extra = new ItemRecord(3, "Cool Sunglasses", 1, null, null, null, null);
82+
var a = new ItemRecord(1, "Sunglasses", 1, null, null, null, null, null, null);
83+
var b = new ItemRecord(2, "Sunglasses", 1, null, null, null, null, null, null);
84+
var extra = new ItemRecord(3, "Cool Sunglasses", 1, null, null, null, null, null, null);
8585
IReadOnlyList<ItemRecord> items = [a, b, extra];
8686
var match = ItemLookup.Resolve("Sunglasses", id => items.FirstOrDefault(i => i.Id == id), items);
8787
var amb = Assert.IsType<ItemMatch.Ambiguous>(match);
@@ -114,7 +114,8 @@ public void EmptyOrWhitespace_NotFound(string q)
114114
[Fact]
115115
public void Ambiguous_RespectsCap()
116116
{
117-
var many = Enumerable.Range(1, 50).Select(i => new ItemRecord(i, $"Gun {i}", 1, null, null, null, null))
117+
var many = Enumerable.Range(1, 50)
118+
.Select(i => new ItemRecord(i, $"Gun {i}", 1, null, null, null, null, null, null))
118119
.ToList();
119120
var match = ItemLookup.Resolve("gun", id => many.FirstOrDefault(x => x.Id == id), many, cap: 10);
120121
var amb = Assert.IsType<ItemMatch.Ambiguous>(match);

tests/RustPlusBot.ItemData.Generator.Tests/DatasetValidatorTests.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@ namespace RustPlusBot.ItemData.Generator.Tests;
77
public sealed class DatasetValidatorTests
88
{
99
private static ItemDataset Good() => new(1,
10-
new DatasetSources(new(2026, 4, 8), new(2024, 9, 7), new(2024, 9, 7), new(2024, 9, 7)),
10+
new DatasetSources(new(2026, 4, 8), new(2024, 9, 7), new(2024, 9, 7), new(2024, 9, 7), new(2024, 9, 7),
11+
new(2024, 9, 7)),
1112
[
1213
new ItemRecord(1, "AK-47", 1, 3600,
13-
new RecycleYield([new YieldEntry(2, 4, 1.0)]), null, null),
14-
new ItemRecord(2, "Metal Fragments", 1000, null, null, null, null),
14+
new RecycleYield([new YieldEntry(2, 4, 1.0)]), null, null, null, null),
15+
new ItemRecord(2, "Metal Fragments", 1000, null, null, null, null, null, null),
1516
]);
1617

1718
/// <summary>A dataset with all referential constraints satisfied should produce no errors.</summary>
@@ -37,7 +38,7 @@ public void UnresolvableYieldId_isError()
3738
var bad = new ItemDataset(1, Good().Sources,
3839
[
3940
new ItemRecord(1, "AK-47", 1, null,
40-
new RecycleYield([new YieldEntry(99999, 4, 1.0)]), null, null),
41+
new RecycleYield([new YieldEntry(99999, 4, 1.0)]), null, null, null, null),
4142
]);
4243
var errors = DatasetValidator.Validate(bad, new ValidationOptions(MinItemCount: 1));
4344
Assert.Contains(errors, e => e.Contains("99999", StringComparison.Ordinal));
@@ -50,7 +51,7 @@ public void UnresolvableCraftIngredientId_isError()
5051
var bad = new ItemDataset(1, Good().Sources,
5152
[
5253
new ItemRecord(1, "AK-47", 1, null, null,
53-
new CraftRecipe([new Ingredient(88888, 100)], 30.0, 3), null),
54+
new CraftRecipe([new Ingredient(88888, 100)], 30.0, 3), null, null, null),
5455
]);
5556
var errors = DatasetValidator.Validate(bad, new ValidationOptions(MinItemCount: 1));
5657
Assert.Contains(errors, e => e.Contains("88888", StringComparison.Ordinal));

tools/RustPlusBot.ItemData.Generator/Program.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,13 +91,13 @@ internal static int Main(string[] args)
9191
var recycle = recycleYields.TryGetValue(id, out var ry) ? ry : null;
9292
var craft = craftRecipes.TryGetValue(id, out var cr) ? cr : null;
9393
var research = researchCosts.TryGetValue(id, out var rc) ? rc : null;
94-
return new ItemRecord(id, name, stackSize, despawn, recycle, craft, research);
94+
return new ItemRecord(id, name, stackSize, despawn, recycle, craft, research, null, null);
9595
})
9696
.ToList();
9797

9898
var dataset = new ItemDataset(
9999
1,
100-
new DatasetSources(NamesAsOf, RecycleAsOf, CraftAsOf, ResearchAsOf),
100+
new DatasetSources(NamesAsOf, RecycleAsOf, CraftAsOf, ResearchAsOf, new(2024, 9, 7), new(2024, 9, 7)),
101101
items);
102102

103103
var validationOptions = new ValidationOptions(MinItemCount: minItems);

0 commit comments

Comments
 (0)