-
Notifications
You must be signed in to change notification settings - Fork 4
/
parse.js
executable file
·306 lines (253 loc) · 9.05 KB
/
parse.js
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
#!/usr/bin/env node
// Core modules
const fs = require('fs');
const path = require('path');
// Custom modules
const jsonfile = require('jsonfile');
const Xnb = require('xnbcli/app/Xnb');
const xnbLog = require('xnbcli/app/Log');
// eslint-disable-next-line no-bitwise
xnbLog.setMode(xnbLog.INFO | xnbLog.WARN | xnbLog.DEBUG, false);
// Constants (data from decompiled Stardew Valley.exe v1.2.30)
const GIANT_CROP_IDS = [190, 254, 276];
const YEAR_2_CROP_IDS = [476, 485, 489];
const GENERAL_STORE_STOCK_IDS = [299, 301, 302, 425, 427, 429, 431, 453, 455, 472, 473, 474, 475,
476, 477, 479, 480, 481, 482, 483, 484, 485, 487, 488, 489, 490, 491, 492, 493];
const JOJAMART_STOCK_IDS = [245, 246, 297, 299, 301, 302, 423, 425, 427, 429, 431, 453, 455, 472,
473, 474, 475, 477, 479, 480, 482, 483, 484, 487, 488, 490, 491, 492, 493];
const NOT_SOLD_AT_TRAVELING_CART_IDS = [158, 159, 160, 161, 162, 163, 326, 341, 413, 437, 439, 454,
460, 645, 680, 681, 682, 688, 689, 690, 774, 775, 802];
const OASIS_STOCK_IDS = [478, 486, 494, 802];
const OUTPUT_PATH = path.resolve(__dirname, 'parsed-crop-data.json');
const CROPS_PATH = path.resolve(__dirname, 'Crops.xnb');
const OBJECT_INFORMATION_PATH = path.resolve(__dirname, 'ObjectInformation.xnb');
// Check Stardew Valley data files exist
[CROPS_PATH, OBJECT_INFORMATION_PATH].forEach((filePath) => {
if (!fs.existsSync(filePath)) {
console.error(`Stardew Valley data file '${filePath}' does not exist`);
process.exit();
}
});
// Load Stardew Valley data files
const crops = new Xnb().load(CROPS_PATH);
const objectInformation = new Xnb().load(OBJECT_INFORMATION_PATH);
// Test format of data files, to detect potential changes/compatability
Object.entries(crops.content).forEach((entry) => {
const [key, value] = entry;
if (!/^\d+$/.test(key)) console.error(`Unexpected format of key '${key}' in crops.content (${CROPS_PATH})`);
if (!/^[\d ]+\/(?:(?:spring|summer|fall|winter) ?)+\/\d+\/\d+\/-?\d+\/[01]\/(?:true \d \d \d+ ?.\d+|false)\/(?:true|false)\/(?:true(?: \d+)+|false)$/.test(value)) console.error(`Unexpected format of value '${value}' in crops.content[${key}] (${CROPS_PATH})`);
});
Object.entries(objectInformation.content).forEach((entry) => {
const [key, value] = entry;
if (!/^\d+$/.test(key)) console.error(`Unexpected format of key '${key}' in objectInformation.content (${OBJECT_INFORMATION_PATH})`);
if (!/^.+\/\d+\/-?\d+\/\w+(?: -\d+)?\/.+\/.+$/.test(value)) console.error(`Unexpected format of value '${value}' in objectInformation.content[${key}] (${OBJECT_INFORMATION_PATH})`);
});
const output = {};
Object.keys(crops.content).forEach((key) => {
const cropData = crops.content[key].split('/');
const cropInfoData = objectInformation.content[cropData[3]].split('/');
const seedInfoData = objectInformation.content[key].split('/');
const cropHarvestData = cropData[6].split(' ').map(Number);
// Remove the 'true' in 'true 3 5 5 .02'
cropHarvestData.shift();
const cropFlowerColorData = cropData[8].split(' ').map(Number);
// Remove the 'true' in
// 'true 35 127 255 109 131 255 112 207 255 191 228 255 94 121 255 40 150 255'
cropFlowerColorData.shift();
const crop = {};
/**
* Crop's name
* @type {string}
*/
crop.name = cropInfoData[0];
/**
* Crop's description
* @type {string}
*/
crop.description = cropInfoData[5];
/**
* Crop's ID and key in ObjectInformation.content and index in springobjects.png tilesheet
* @type {number}
*/
crop.id = Number(cropData[3]);
/**
* Crop's category in player's inventory
* @type {string}
*/
crop.category = cropInfoData[3];
/**
* Crop's sell value
* @type {number}
*/
crop.sellPrice = Number(cropInfoData[1]);
/**
* Amount of health restored on consumption
* Amount of energy restored on consumption is this * 2.5
* Negative values cause energy loss but not health loss
* For non-consumable crops this value will be -300
* Named after decompiled Crop.cs from Stardew Valley.exe
* @type {number}
*/
crop.edibility = Number(cropInfoData[2]);
/**
* Crop's index in crops.png tilesheet
* Named after decompiled Crop.cs from Stardew Valley.exe
* @type {number}
*/
crop.rowInSpriteSheet = Number(cropData[2]);
/**
* Seasons crop can grow in
* Named after decompiled Crop.cs from Stardew Valley.exe
* @type {string[]}
*/
crop.seasonsToGrowIn = cropData[1].split(' ');
/**
* Each item represents days spent in each stage of growth
* Named after decompiled Crop.cs from Stardew Valley.exe
* @type {number[]}
*/
crop.phaseDays = cropData[0].split(' ').map(Number);
/**
* Days taken after first harvest for crop to produce again. If crop doesn't produce again this
* will be -1
* Named after decompiled Crop.cs from Stardew Valley.exe
* @type {number}
*/
crop.regrowAfterHarvest = Number(cropData[4]);
/**
* Is the crop harvested with the scythe
* @type {boolean}
*/
crop.scythe = Boolean(Number(cropData[5]));
/**
* Does the crop grow on a trellis. May technically be whether the crop has a solid bounding box
* @type {boolean}
*/
crop.trellis = (cropData[7] === 'true');
/**
* Whether the crop can become giant
* @type {boolean}
*/
crop.canBeGiant = (GIANT_CROP_IDS.indexOf(crop.id) > -1);
crop.harvest = {};
if (cropHarvestData.length === 4) {
/**
* Minimum yield from each harvest
* @type {number}
*/
crop.harvest.minHarvest = cropHarvestData[0];
/**
* Maximum yield from each harvest
* @type {number}
*/
crop.harvest.maxHarvest = cropHarvestData[1];
/**
* Field purpose not entirely known. Named after decompiled Crop.cs from Stardew Valley.exe
* @type {number}
*/
crop.harvest.maxHarvestIncreasePerFarmingLevel = cropHarvestData[2];
/**
* Field purpose not entirely known. Named after decompiled Crop.cs from Stardew Valley.exe
* @type {number}
*/
crop.harvest.chanceForExtraCrops = cropHarvestData[3];
}
crop.seed = {};
/**
* Seed's name
* @type {string}
*/
crop.seed.name = seedInfoData[0];
/**
* Seed's description
* @type {string}
*/
crop.seed.description = seedInfoData[5];
/**
* Seed's ID
* @type {number}
*/
crop.seed.id = Number(key);
/**
* Seed's category. In Stardew Valley v1.11 this is always 'Seeds -74'
* @type {string}
*/
crop.seed.category = seedInfoData[3];
/**
* Seed's sell value
* @type {number}
*/
crop.seed.sellPrice = Number(seedInfoData[1]);
/**
* Would be the amount of health restored on consumption
* However no seeds are consumable so will always be -300
* Named after decompiled Crop.cs from Stardew Valley.exe
* @type {number}
*/
crop.seed.edibility = Number(seedInfoData[2]);
crop.seed.vendor = {};
// Seeds sold at Pierre's General Store
if (GENERAL_STORE_STOCK_IDS.indexOf(crop.seed.id) > -1) {
crop.seed.vendor.generalStore = {
price: crop.seed.sellPrice * 2,
yearAvailable: 1,
};
// Correct Sunflower Seeds price
if (crop.seed.id === 431) crop.seed.vendor.generalStore.price = 200;
// Correct year available for Garlic Seeds, Red Cabbage Seeds and Artichoke Seeds
if (YEAR_2_CROP_IDS.indexOf(crop.seed.id) > -1) crop.seed.vendor.generalStore.yearAvailable = 2;
}
// Seeds sold at JojaMart
if (JOJAMART_STOCK_IDS.indexOf(crop.seed.id) > -1) {
crop.seed.vendor.jojaMart = {
price: Math.floor(crop.seed.sellPrice * 2.5),
};
// Correct Sunflower Seeds price
if (crop.seed.id === 431) crop.seed.vendor.jojaMart.price = 125;
}
// Seeds sold at the Traveling Cart
if (NOT_SOLD_AT_TRAVELING_CART_IDS.indexOf(crop.seed.id) === -1 && crop.seed.sellPrice > 0) {
crop.seed.vendor.travelingCart = {
minPrice: Math.max(1 * 100, crop.seed.sellPrice * 3),
maxPrice: Math.max(10 * 100, crop.seed.sellPrice * 5),
};
}
// Rare Seed (grows Sweet Gem Berry)
if (crop.seed.id === 347) crop.seed.vendor.travelingCart.price = 1000;
// Coffee Bean
else if (crop.seed.id === 433) crop.seed.vendor.travelingCart.price = 2500;
// Seeds sold at Oasis (Desert shop)
if (OASIS_STOCK_IDS.indexOf(crop.seed.id) > -1) {
crop.seed.vendor.oasis = {
price: crop.seed.sellPrice * 2,
};
// Correct Cactus Seeds price
if (crop.seed.id === 802) crop.seed.vendor.oasis.price = 150;
}
// Seeds sold at the Egg Festival (Strawberry Seeds)
if (crop.seed.id === 745) {
crop.seed.vendor.eggFestival = {
price: 100,
};
}
/**
* Crop's flower colors
* @type {{red: number, green: number, blue: number}}
*/
crop.flowerColors = (() => {
const colors = [];
for (let i = 0; i < cropFlowerColorData.length / 3; i += 1) {
const j = i * 3;
colors.push({
red: cropFlowerColorData[j],
green: cropFlowerColorData[j + 1],
blue: cropFlowerColorData[j + 2],
});
}
return colors;
})();
output[crop.id] = crop;
});
jsonfile.writeFile(OUTPUT_PATH, output, { spaces: 2 }).then(() => {
console.log(`🌱 Parsed ${Object.keys(output).length} crops and saved to '${OUTPUT_PATH}'`);
});