Skip to content

Commit 5a1428f

Browse files
authored
fix: Add limits to how much world processing will process will happen through the validator (#94)
- Improve the popular filter
1 parent 0204d99 commit 5a1428f

File tree

46 files changed

+269
-201
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+269
-201
lines changed

app/src/UX/components/projectGrid/ProjectGrid.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ export default function ProjectGrid({ onAppGalleryAction }: ProjectGridProps) {
5454
</Typography>
5555
{["entity", "blocks", "items", "typescript", "spawn"].map((tag) => {
5656
const isActive = searchQuery === tag;
57+
58+
if (tag && tag.endsWith("s")) {
59+
tag = tag.slice(0, -1); // Remove trailing 's'
60+
}
61+
5762
return (
5863
<Box
5964
key={tag}

app/src/info/WorldDataInfoGenerator.ts

Lines changed: 149 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import { GameType } from "../minecraft/WorldLevelDat";
2323

2424
import { IGeneratorOptions } from "./ProjectInfoSet";
2525

26+
export const MaxWorldRecordsToProcess = 3000000; // very crudely, this equates to about 100K chunks
27+
2628
export enum WorldDataInfoGeneratorTest {
2729
unexpectedCommandInMCFunction = 101,
2830
unexpectedCommandInCommandBlock = 102,
@@ -40,6 +42,7 @@ export enum WorldDataInfoGeneratorTest {
4042
subchunklessChunks = 127,
4143
chunks = 128,
4244
commandIsFromOlderMinecraftVersion = 212,
45+
couldNotProcessWorld = 216,
4346
errorProcessingWorld = 400,
4447
unexpectedError = 401,
4548
}
@@ -67,6 +70,9 @@ export default class WorldDataInfoGenerator implements IProjectInfoItemGenerator
6770
WorldDataInfoGeneratorTest.subchunklessChunks
6871
);
6972

73+
info.completedWorldDataProcessing =
74+
infoSet.getCount("WORLDDATA", WorldDataInfoGeneratorTest.couldNotProcessWorld) === 0;
75+
7076
info.worldLoadErrors = infoSet.getCount("WORLDDATA", WorldDataInfoGeneratorTest.errorProcessingWorld);
7177

7278
const levelItems = infoSet.getItems(this.id, WorldDataInfoGeneratorTest.levelDat);
@@ -360,7 +366,9 @@ export default class WorldDataInfoGenerator implements IProjectInfoItemGenerator
360366

361367
await mcworld.loadMetaFiles(false);
362368

363-
await mcworld.loadLevelDb(false);
369+
let didProcessWorldData = await mcworld.loadLevelDb(false, {
370+
maxNumberOfRecordsToProcess: MaxWorldRecordsToProcess,
371+
});
364372

365373
if (
366374
mcworld.isInErrorState &&
@@ -384,6 +392,24 @@ export default class WorldDataInfoGenerator implements IProjectInfoItemGenerator
384392
)
385393
);
386394
}
395+
396+
didProcessWorldData = false;
397+
}
398+
399+
if (!didProcessWorldData) {
400+
items.push(
401+
new ProjectInfoItem(
402+
InfoItemType.info,
403+
this.id,
404+
WorldDataInfoGeneratorTest.couldNotProcessWorld,
405+
ProjectInfoUtilities.getTitleFromEnum(
406+
WorldDataInfoGeneratorTest,
407+
WorldDataInfoGeneratorTest.couldNotProcessWorld
408+
),
409+
projectItem,
410+
mcworld.name
411+
)
412+
);
387413
}
388414

389415
if (
@@ -458,141 +484,155 @@ export default class WorldDataInfoGenerator implements IProjectInfoItemGenerator
458484
)
459485
);
460486

461-
let blockCount = 0;
462-
let chunkCount = 0;
463-
let subchunkLessChunkCount = 0;
464-
465-
// Use the memory-efficient chunk iterator with aggressive data clearing
466-
// This prevents heap exhaustion on large worlds by clearing chunk data after processing
467-
await mcworld.forEachChunk(
468-
async (chunk, x, z, dimIndex) => {
469-
chunkCount++;
487+
if (didProcessWorldData) {
488+
let blockCount = 0;
489+
let chunkCount = 0;
490+
let subchunkLessChunkCount = 0;
470491

471-
if (chunk.subChunks.length <= 0) {
472-
subchunkLessChunkCount++;
473-
}
492+
// Use the memory-efficient chunk iterator with aggressive data clearing
493+
// This prevents heap exhaustion on large worlds by clearing chunk data after processing
494+
await mcworld.forEachChunk(
495+
async (chunk, x, z, dimIndex) => {
496+
chunkCount++;
474497

475-
const blockActors = chunk.blockActors;
498+
if (chunk.subChunks.length <= 0) {
499+
subchunkLessChunkCount++;
500+
}
476501

477-
for (let i = 0; i < blockActors.length; i++) {
478-
const blockActor = blockActors[i];
502+
const blockActors = chunk.blockActors;
479503

480-
if (blockActor.id) {
481-
blockActorsPi.incrementFeature(blockActor.id);
482-
}
504+
for (let i = 0; i < blockActors.length; i++) {
505+
const blockActor = blockActors[i];
483506

484-
if (blockActor instanceof CommandBlockActor) {
485-
let cba = blockActor as CommandBlockActor;
486-
if (cba.version) {
487-
blockActorsPi.spectrumIntFeature("Command Version", cba.version);
507+
if (blockActor.id) {
508+
blockActorsPi.incrementFeature(blockActor.id);
488509
}
489510

490-
if (cba.version && cba.version < this.modernCommandVersion) {
491-
items.push(
492-
new ProjectInfoItem(
493-
InfoItemType.recommendation,
494-
this.id,
495-
WorldDataInfoGeneratorTest.commandIsFromOlderMinecraftVersion,
496-
"Command '" + cba.command + "' is from an older Minecraft version (" + cba.version + ") ",
497-
projectItem,
498-
"(Command at location " + cba.x + ", " + cba.y + ", " + cba.z + ")",
499-
undefined,
500-
cba.command
501-
)
502-
);
503-
}
511+
if (blockActor instanceof CommandBlockActor) {
512+
let cba = blockActor as CommandBlockActor;
513+
if (cba.version) {
514+
blockActorsPi.spectrumIntFeature("Command Version", cba.version);
515+
}
516+
517+
if (cba.version && cba.version < this.modernCommandVersion) {
518+
items.push(
519+
new ProjectInfoItem(
520+
InfoItemType.recommendation,
521+
this.id,
522+
WorldDataInfoGeneratorTest.commandIsFromOlderMinecraftVersion,
523+
"Command '" + cba.command + "' is from an older Minecraft version (" + cba.version + ") ",
524+
projectItem,
525+
"(Command at location " + cba.x + ", " + cba.y + ", " + cba.z + ")",
526+
undefined,
527+
cba.command
528+
)
529+
);
530+
}
504531

505-
if (cba.command && cba.command.trim().length > 2) {
506-
let command = CommandStructure.parse(cba.command);
532+
if (cba.command && cba.command.trim().length > 2) {
533+
let command = CommandStructure.parse(cba.command);
534+
535+
if (CommandRegistry.isMinecraftBuiltInCommand(command.fullName)) {
536+
if (this.performAddOnValidations && CommandRegistry.isAddOnBlockedCommand(command.fullName)) {
537+
items.push(
538+
new ProjectInfoItem(
539+
InfoItemType.warning,
540+
this.id,
541+
WorldDataInfoGeneratorTest.containsWorldImpactingCommand,
542+
"Contains command '" +
543+
command.fullName +
544+
"' which is impacts the state of the entire world, and generally shouldn't be used in an add-on",
545+
projectItem,
546+
command.fullName,
547+
undefined,
548+
cba.command
549+
)
550+
);
551+
}
507552

508-
if (CommandRegistry.isMinecraftBuiltInCommand(command.fullName)) {
509-
if (this.performAddOnValidations && CommandRegistry.isAddOnBlockedCommand(command.fullName)) {
553+
commandsPi.incrementFeature(command.fullName);
554+
555+
if (command.fullName === "execute") {
556+
let foundRun = false;
557+
for (const arg of command.commandArguments) {
558+
if (arg === "run") {
559+
foundRun = true;
560+
} else if (foundRun && CommandRegistry.isMinecraftBuiltInCommand(arg)) {
561+
subCommandsPi.incrementFeature(arg);
562+
break;
563+
}
564+
}
565+
}
566+
} else if (!this.performAddOnValidations && !this.performPlatformVersionValidations) {
510567
items.push(
511568
new ProjectInfoItem(
512-
InfoItemType.warning,
569+
InfoItemType.error,
513570
this.id,
514-
WorldDataInfoGeneratorTest.containsWorldImpactingCommand,
515-
"Contains command '" +
516-
command.fullName +
517-
"' which is impacts the state of the entire world, and generally shouldn't be used in an add-on",
571+
WorldDataInfoGeneratorTest.unexpectedCommandInCommandBlock,
572+
"Unexpected command '" + command.fullName + "'",
518573
projectItem,
519574
command.fullName,
520575
undefined,
521576
cba.command
522577
)
523578
);
524579
}
525-
526-
commandsPi.incrementFeature(command.fullName);
527-
528-
if (command.fullName === "execute") {
529-
let foundRun = false;
530-
for (const arg of command.commandArguments) {
531-
if (arg === "run") {
532-
foundRun = true;
533-
} else if (foundRun && CommandRegistry.isMinecraftBuiltInCommand(arg)) {
534-
subCommandsPi.incrementFeature(arg);
535-
break;
536-
}
537-
}
538-
}
539-
} else if (!this.performAddOnValidations && !this.performPlatformVersionValidations) {
540-
items.push(
541-
new ProjectInfoItem(
542-
InfoItemType.error,
543-
this.id,
544-
WorldDataInfoGeneratorTest.unexpectedCommandInCommandBlock,
545-
"Unexpected command '" + command.fullName + "'",
546-
projectItem,
547-
command.fullName,
548-
undefined,
549-
cba.command
550-
)
551-
);
552580
}
553581
}
554582
}
555-
}
556583

557-
// Use memory-efficient block type counting instead of getBlockList()
558-
// This avoids allocating massive arrays of Block objects
559-
const blockTypeCounts = chunk.countBlockTypes();
584+
// Use memory-efficient block type counting instead of getBlockList()
585+
// This avoids allocating massive arrays of Block objects
586+
const blockTypeCounts = chunk.countBlockTypes();
560587

561-
for (const [typeName, count] of blockTypeCounts) {
562-
blockCount += count;
588+
for (const [typeName, count] of blockTypeCounts) {
589+
blockCount += count;
563590

564-
let type = typeName;
565-
if (type.indexOf(":") >= 0 && type.indexOf("minecraft:") < 0) {
566-
type = "(custom)";
567-
}
591+
let type = typeName;
592+
if (type.indexOf(":") >= 0 && type.indexOf("minecraft:") < 0) {
593+
type = "(custom)";
594+
}
568595

569-
blocksPi.incrementFeature(type, "count", count);
570-
}
571-
},
572-
{
573-
// Always clear parsed/cached data after processing each chunk to prevent OOM.
574-
// This is non-destructive - raw LevelKeyValue bytes are preserved, so chunks
575-
// can be re-parsed on demand (e.g., when the world map needs them).
576-
clearCacheAfterProcess: true,
577-
// Only clear raw LevelKeyValue data when in aggressive cleanup mode (CLI validation).
578-
// In browser contexts, we preserve raw data so the world map can re-parse chunks.
579-
clearAllAfterProcess: performAggressiveCleanup,
580-
progressCallback: async (processed, total) => {
581-
// Use worldName captured from outside closure to avoid TypeScript null check issue
582-
const worldName = mcworld?.name ?? "unknown";
583-
let mess =
584-
"World data validation: scanned " +
585-
Math.floor(processed / 1000) +
586-
"K of " +
587-
Math.floor(total / 1000) +
588-
"K chunks in " +
589-
worldName;
590-
await projectItem.project.creatorTools.notifyStatusUpdate(mess, StatusTopic.validation);
596+
blocksPi.incrementFeature(type, "count", count);
597+
}
591598
},
592-
}
593-
);
594-
595-
blocksPi.data = blockCount;
599+
{
600+
// Always clear parsed/cached data after processing each chunk to prevent OOM.
601+
// This is non-destructive - raw LevelKeyValue bytes are preserved, so chunks
602+
// can be re-parsed on demand (e.g., when the world map needs them).
603+
clearCacheAfterProcess: true,
604+
// Only clear raw LevelKeyValue data when in aggressive cleanup mode (CLI validation).
605+
// In browser contexts, we preserve raw data so the world map can re-parse chunks.
606+
clearAllAfterProcess: performAggressiveCleanup,
607+
progressCallback: async (processed, total) => {
608+
// Use worldName captured from outside closure to avoid TypeScript null check issue
609+
const worldName = mcworld?.name ?? "unknown";
610+
let mess =
611+
"World data validation: scanned " +
612+
Math.floor(processed / 1000) +
613+
"K of " +
614+
Math.floor(total / 1000) +
615+
"K chunks in " +
616+
worldName;
617+
await projectItem.project.creatorTools.notifyStatusUpdate(mess, StatusTopic.validation);
618+
},
619+
}
620+
);
621+
622+
items.push(
623+
new ProjectInfoItem(
624+
InfoItemType.info,
625+
this.id,
626+
WorldDataInfoGeneratorTest.subchunklessChunks,
627+
"Subchunkless Chunks",
628+
projectItem,
629+
subchunkLessChunkCount,
630+
mcworld.name
631+
)
632+
);
633+
634+
blocksPi.data = blockCount;
635+
}
596636

597637
items.push(
598638
new ProjectInfoItem(
@@ -638,17 +678,6 @@ export default class WorldDataInfoGenerator implements IProjectInfoItemGenerator
638678
mcworld.name
639679
)
640680
);
641-
items.push(
642-
new ProjectInfoItem(
643-
InfoItemType.info,
644-
this.id,
645-
WorldDataInfoGeneratorTest.subchunklessChunks,
646-
"Subchunkless Chunks",
647-
projectItem,
648-
subchunkLessChunkCount,
649-
mcworld.name
650-
)
651-
);
652681
}
653682

654683
return items;

0 commit comments

Comments
 (0)