Skip to content

Commit 9d33bdb

Browse files
committed
fix critical optimizer bug and also cleaned it up
1 parent 5e76bf6 commit 9d33bdb

File tree

1 file changed

+139
-82
lines changed

1 file changed

+139
-82
lines changed

src/lib/text/nbt/optimiser.ts

Lines changed: 139 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,131 @@ export function optimise(
3333
output.push("");
3434
}
3535

36-
// 0: Early return for single string
36+
// Early return for single string
3737
if (
3838
stringyTextElements.length === 1 &&
3939
typeof stringyTextElements[0] === "string"
4040
) {
4141
return stringyTextElements;
4242
}
4343

44-
// 1: Remove undefineds, flatten MinecraftText with only text
44+
output.push(...flattenMCText(stringyTextElements));
45+
output = mergeTextComponents(output);
46+
47+
// remove leading empty string if followed by a string
48+
if (output.length >= 2 && output[0] === "" && typeof output[1] === "string")
49+
output.shift();
50+
51+
if (shouldHaveLeadingEmptyString(output)) {
52+
output.shift();
53+
}
54+
55+
// if it is item lore then override
56+
if (lore) {
57+
output.unshift({ italic: false, color: "white", text: "" });
58+
}
59+
return output;
60+
}
61+
62+
/**
63+
* Gets all shared style properties between two MinecraftText components
64+
*
65+
* @param a text component a to check
66+
* @param b text component b to check
67+
* @returns a record of shared style properties
68+
*/
69+
function getSharedStyleProps(
70+
a: MinecraftText,
71+
b: MinecraftText,
72+
): Record<keyof MinecraftText, any> {
73+
const allSharedProps: Record<keyof MinecraftText, any> = {} as Record<
74+
keyof MinecraftText,
75+
any
76+
>;
77+
for (const prop of styleProps) {
78+
const p = prop as MCTextKey;
79+
if (
80+
a[p] !== undefined &&
81+
b[p] !== undefined &&
82+
propsMatch(a[p], b[p], prop)
83+
) {
84+
allSharedProps[p] = a[p];
85+
}
86+
}
87+
return allSharedProps;
88+
}
89+
90+
/**
91+
* Collects all MinecraftText components from the current index that share the same style properties
92+
*
93+
* @param i the current index into the array
94+
* @param group the current group of MinecraftText components being collected
95+
* @param output the current output array
96+
* @param allSharedProps a record of all shared properties between the group
97+
*/
98+
function collectAllFromIndex(
99+
i: number,
100+
group: MinecraftText[],
101+
output: StringyMCText[],
102+
allSharedProps: Record<keyof MinecraftText, any>,
103+
) {
104+
let j = i;
105+
let sharedKeys = Object.keys(allSharedProps) as (keyof MinecraftText)[];
106+
while (output[j + 1] && typeof output[j + 1] === "object") {
107+
const next = output[j + 1] as MinecraftText;
108+
let allPropertiesMatch = sharedKeys.every(
109+
(prop) =>
110+
next[prop] !== undefined &&
111+
propsMatch(next[prop], allSharedProps[prop], prop),
112+
);
113+
114+
if (!allPropertiesMatch) break;
115+
116+
group.push(next);
117+
j++;
118+
}
119+
}
120+
121+
/**
122+
* Checks if two properties match, considering interactive properties
123+
*
124+
* @param a property A
125+
* @param b property B
126+
* @param property the property to check for
127+
* @returns true if the properties match, false otherwise
128+
*/
129+
function propsMatch(a: any, b: any, property: string) {
130+
return isAnInteractiveProp(property)
131+
? JSON.stringify(a) === JSON.stringify(b)
132+
: a === b;
133+
}
134+
135+
/**
136+
* Determines if the final output should have the leading empty string removed
137+
*
138+
* @param output the optimized output array
139+
* @returns true if the final output should have the leading empty string removed
140+
*/
141+
function shouldHaveLeadingEmptyString(output: StringyMCText[]): boolean {
142+
return (
143+
output.length >= 2 &&
144+
output[0] == "" &&
145+
(typeof output[1] === "string" ||
146+
(typeof output[1] === "object" &&
147+
!styleProps.some(
148+
(prop) => output[1][prop as keyof StringyMCText] !== undefined,
149+
)))
150+
);
151+
}
152+
153+
/**
154+
* Flattens the text elements by converting objects with only text property to strings and removing undefined properties
155+
*
156+
* @param stringyTextElements the text elements
157+
* @returns the elements with strings becoming string literals and cleaned object properties
158+
*/
159+
function flattenMCText(stringyTextElements: StringyMCText[]): StringyMCText[] {
160+
const output: StringyMCText[] = [];
45161
for (const component of stringyTextElements) {
46162
if (typeof component === "string") {
47163
// string, just add
@@ -63,8 +179,16 @@ export function optimise(
63179
Object.keys(component).length === 1 ? component.text! : component,
64180
);
65181
}
182+
return output;
183+
}
66184

67-
// 2: Merge adjacent strings and whitespace, group objects with shared style
185+
/**
186+
* Merges adjacent strings and whitespace, groups objects with shared style/interactivity properties
187+
*
188+
* @param output the optimized output step
189+
* @returns the optimized output step with merged strings, whitespace and group properties with shared styling
190+
*/
191+
function mergeTextComponents(output: StringyMCText[]) {
68192
for (let i = 0; i < output.length - 1; i++) {
69193
const current = output[i],
70194
next = output[i + 1];
@@ -108,21 +232,18 @@ export function optimise(
108232
// Find shared style/interactivity properties between consecutive objects
109233
if (typeof current === "object" && typeof next === "object") {
110234
// Merge all properties in styleProps that are identical across the group
111-
const allShared: Record<keyof MinecraftText, any> = getSharedStyleProps(
112-
current,
113-
next,
114-
);
235+
const sharedProperties = getSharedStyleProps(current, next);
115236

116-
if (Object.keys(allShared).length > 0) {
237+
if (Object.keys(sharedProperties).length > 0) {
117238
// Find how many consecutive objects share these properties
118239
let group = [current];
119240

120-
collectAllFromIndex(i, group, output, allShared);
241+
collectAllFromIndex(i, group, output, sharedProperties);
121242
if (group.length > 1) {
122243
// Remove shared properties from each group member for "extra"
123244
let extras: StringyMCText[] = group.map((comp) => {
124245
const copy = { ...comp };
125-
for (const prop of Object.keys(allShared)) {
246+
for (const prop of Object.keys(sharedProperties)) {
126247
delete copy[prop as MCTextKey];
127248
}
128249
return copy;
@@ -131,13 +252,18 @@ export function optimise(
131252
// Optimise extra
132253
extras = optimise(extras);
133254
const first = extras.shift();
134-
let merged = { ...allShared };
255+
let merged = { ...sharedProperties };
256+
257+
// Rebuild merged component
135258
if (typeof first == "string") {
136259
merged.text = first;
137260
if (extras.length > 0) merged.extra = extras;
138261
} else {
139-
merged = { ...allShared, ...first };
140-
if (extras.length > 0) merged.extra = extras;
262+
Object.assign(merged, { ...first });
263+
if (extras.length > 0) {
264+
if (!merged.extra) merged.extra = extras;
265+
else merged.extra = merged.extra.concat(extras); // this single line cost me 20 minutes
266+
}
141267
}
142268
output.splice(i, group.length, merged);
143269
i--; // recheck at this position
@@ -146,74 +272,5 @@ export function optimise(
146272
}
147273
}
148274
}
149-
150-
// 3: Remove leading empty string if followed by a string
151-
if (output.length >= 2 && output[0] === "" && typeof output[1] === "string")
152-
output.shift();
153-
154-
// 4: If out[1] is a string, or an object without any style properties, then remove out[0]
155-
if (
156-
output.length >= 2 &&
157-
output[0] == "" &&
158-
(typeof output[1] === "string" ||
159-
(typeof output[1] === "object" &&
160-
!styleProps.some(
161-
(prop) => output[1][prop as keyof StringyMCText] !== undefined,
162-
)))
163-
) {
164-
output.shift();
165-
}
166-
167-
// 5: If it is item lore then override
168-
if (lore) {
169-
output.unshift({ italic: false, color: "white", text: "" });
170-
}
171275
return output;
172276
}
173-
174-
function getSharedStyleProps(a: MinecraftText, b: MinecraftText) {
175-
const allSharedProps: Record<keyof MinecraftText, any> = {} as Record<
176-
keyof MinecraftText,
177-
any
178-
>;
179-
for (const prop of styleProps) {
180-
const p = prop as MCTextKey;
181-
if (
182-
a[p] !== undefined &&
183-
b[p] !== undefined &&
184-
propsMatch(a[p], b[p], prop)
185-
) {
186-
allSharedProps[p] = a[p];
187-
}
188-
}
189-
return allSharedProps;
190-
}
191-
192-
function collectAllFromIndex(
193-
i: number,
194-
group: MinecraftText[],
195-
output: StringyMCText[],
196-
allSharedProps: Record<keyof MinecraftText, any>,
197-
) {
198-
let j = i;
199-
let sharedKeys = Object.keys(allSharedProps) as (keyof MinecraftText)[];
200-
while (output[j + 1] && typeof output[j + 1] === "object") {
201-
const next = output[j + 1] as MinecraftText;
202-
let allPropertiesMatch = sharedKeys.every(
203-
(prop) =>
204-
next[prop] !== undefined &&
205-
propsMatch(next[prop], allSharedProps[prop], prop),
206-
);
207-
208-
if (!allPropertiesMatch) break;
209-
210-
group.push(next);
211-
j++;
212-
}
213-
}
214-
215-
function propsMatch(a: any, b: any, property: string) {
216-
return isAnInteractiveProp(property)
217-
? JSON.stringify(a) === JSON.stringify(b)
218-
: a === b;
219-
}

0 commit comments

Comments
 (0)