Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
3852c0c
feat: add struct grammar rules to sfml
ReinderN Jan 24, 2026
1aa745e
feat: add struct ast classes
ReinderN Jan 24, 2026
c5ea665
refactor: implement structfieldvalue on existing ast classes
ReinderN Jan 24, 2026
48314d3
feat: add struct support to program and labelaccess
ReinderN Jan 24, 2026
586d8eb
feat: add struct visitor methods to astbuilder
ReinderN Jan 24, 2026
1635366
feat: add struct linters
ReinderN Jan 24, 2026
8bac6ed
test: add struct tests
ReinderN Jan 24, 2026
9559894
feat: add protocol, macro, and library support to SFML
ReinderN Jan 24, 2026
d2372c6
feat: add library block for sharing SFML definitions
ReinderN Jan 24, 2026
6274bb6
feat: add linters for protocol and macro usage
ReinderN Jan 24, 2026
b6705d9
feat: add syntax highlighting and localization for new keywords
ReinderN Jan 24, 2026
5eebc38
test: add tests for protocols, macros, and structs
ReinderN Jan 24, 2026
cb49705
feat: add library block GUI, textures, and disk indicator renderer
ReinderN Jan 24, 2026
abf34fe
refactor: remove unused import feature and add templates
ReinderN Jan 25, 2026
a314d63
perf: optimize library block resolution with auto-label cache
ReinderN Jan 25, 2026
bbef8e3
fix: auto-revalidate programs when library blocks change
ReinderN Jan 25, 2026
804a138
refactor: consolidate code duplication and improve maintainability
ReinderN Jan 25, 2026
62f7e42
feat: replace EXPAND keyword with DO and @ symbol for macro invocation
ReinderN Jan 25, 2026
53eab73
style: rename ITEMS constant to items field
ReinderN Jan 25, 2026
d9f26b3
fix: add debouncing for library changes and circular dependency detec…
ReinderN Jan 25, 2026
2fe9c1f
feat: add library block crafting recipe
ReinderN Jan 25, 2026
dbfc900
feat: enable USE statements in library disks and error display
ReinderN Jan 25, 2026
f64f993
feat: improve library block visuals with pulsing indicators and serve…
ReinderN Jan 25, 2026
2ad7702
feat: redesign library GUI to match server rack aesthetic
ReinderN Jan 25, 2026
013f499
fix: recompile libraries and managers on cable network changes
ReinderN Jan 25, 2026
eda119f
feat: integrate LED indicator into disk slot
ReinderN Jan 25, 2026
f64970d
fix: notify disconnected blocks when cable is removed
ReinderN Jan 25, 2026
f5cb7a2
fix: ensure network discovery on world reload and batch notifications
ReinderN Jan 25, 2026
095bb5d
fix: recompile library disks when isolated from network
ReinderN Jan 25, 2026
eb31736
test: add library and definitions tests
ReinderN Jan 25, 2026
39dd822
refactor: simplify struct instantiation syntax
ReinderN Jan 25, 2026
5081e36
docs: add WITH clause example to structs template
ReinderN Jan 25, 2026
a34904d
fix: truncate long library names and add scroll on hover
ReinderN Jan 25, 2026
e141081
Merge remote-tracking branch 'upstream/1.19.2' into feature/library-a…
ReinderN Jan 26, 2026
e9415a9
Merge branch '1.19.2' into feature/library-and-definitions
ReinderN Jan 28, 2026
a713fed
Merge remote-tracking branch 'upstream/1.19.2' into feature/library-a…
ReinderN Jan 30, 2026
3bbca74
Merge remote-tracking branch 'origin/1.19.2' into feature/library-and…
ReinderN Aug 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ protected void registerStatesAndModels() {
registerWaterTank();
registerTestBarrel();
registerBuffer();
registerLibrary();
}

private void registerTestBarrel() {
Expand Down Expand Up @@ -283,4 +284,36 @@ private void registerBuffer() {
});

}

private void registerLibrary() {
if (SFMBlocks.LIBRARY_BLOCK == null) return;

// Create a model with different textures for front, back, and sides
ModelFile libraryModel = models().cube(
SFMBlocks.LIBRARY_BLOCK.getPath(),
modLoc("block/library_bot"), // down
modLoc("block/library_top"), // up
modLoc("block/library_front"), // north (front)
modLoc("block/library_back"), // south (back)
modLoc("block/library_side"), // west
modLoc("block/library_side") // east
).texture("particle", modLoc("block/library_top"));

// Create variants for each horizontal facing direction
getVariantBuilder(SFMBlocks.LIBRARY_BLOCK.get())
.forAllStates(state -> {
Direction facing = state.getValue(ca.teamdman.sfm.common.block.LibraryBlock.FACING);
int yRot = switch (facing) {
case NORTH -> 0;
case EAST -> 90;
case SOUTH -> 180;
case WEST -> 270;
default -> 0;
};
return ConfiguredModel.builder()
.modelFile(libraryModel)
.rotationY(yRot)
.build();
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ protected void registerModels() {
justParent(SFMItems.PRINTING_PRESS, SFMBlocks.PRINTING_PRESS);
justParent(SFMItems.WATER_TANK, SFMBlocks.WATER_TANK, "_active");
justParent(SFMItems.BUFFER, SFMBlocks.BUFFER_BLOCK, "_item");
if (SFMItems.LIBRARY_ITEM != null) {
justParent(SFMItems.LIBRARY_ITEM, SFMBlocks.LIBRARY_BLOCK);
}
basicItem(SFMItems.DISK);
basicItem(SFMItems.LABEL_GUN);
basicItem(SFMItems.EXPERIENCE_GOOP);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ protected void populate(BlockLootWriter writer) {
writer.dropOther(SFMBlocks.FANCY_CABLE_FACADE, SFMBlocks.FANCY_CABLE);
writer.dropSelf(SFMBlocks.PRINTING_PRESS);
writer.dropSelf(SFMBlocks.WATER_TANK);
if (SFMBlocks.LIBRARY_BLOCK != null) {
writer.dropSelf(SFMBlocks.LIBRARY_BLOCK);
}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import ca.teamdman.sfm.common.registry.registration.SFMBlocks;
import ca.teamdman.sfm.common.registry.registration.SFMItems;
import ca.teamdman.sfm.common.registry.registration.SFMRecipeSerializers;
import ca.teamdman.sfm.common.util.SFMEnvironmentUtils;
import ca.teamdman.sfm.common.util.SFMResourceLocation;
import ca.teamdman.sfm.datagen.version_plumbing.MCVersionAgnosticRecipeDataGen;
import net.minecraft.data.recipes.FinishedRecipe;
Expand Down Expand Up @@ -244,6 +245,18 @@ protected void populate(Consumer<FinishedRecipe> writer) {
.pattern("gxg")
.save(writer);

if (SFMEnvironmentUtils.isInIDE() && SFMBlocks.LIBRARY_BLOCK != null) {
beginShaped(SFMBlocks.LIBRARY_BLOCK.get(), 1)
.define('M', SFMBlocks.MANAGER.get())
.define('B', Blocks.BOOKSHELF)
.define('L', Blocks.LECTERN)
.unlockedBy("has_manager", RecipeProvider.has(SFMBlocks.MANAGER.get()))
.pattern("MBM")
.pattern("BLB")
.pattern("MBM")
.save(writer);
}

addPrintingPressRecipe(
writer,
SFMResourceLocation.fromSFMPath("written_book_copy"),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
package ca.teamdman.sfm.gametest;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Classes annotated with this that extend {@link SFMGameTestGeneratorBase} will have their
* {@link SFMGameTestGeneratorBase#generateTests} method invoked to produce game test definitions.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface SFMGameTestGenerator {
}
package ca.teamdman.sfm.gametest;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Classes annotated with this that extend {@link SFMGameTestGeneratorBase} will have their
* {@link SFMGameTestGeneratorBase#generateTests} method invoked to produce game test definitions.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface SFMGameTestGenerator {
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
package ca.teamdman.sfm.gametest;
import java.util.function.Consumer;
/**
* Base class for game test generators. Subclasses annotated with
* {@link SFMGameTestGenerator} will have their {@link #generateTests} method
* invoked during test discovery to produce multiple game test definitions.
*/
public abstract class SFMGameTestGeneratorBase {
/**
* Generates game test definitions and passes them to the provided consumer.
*
* @param testConsumer a consumer that accepts generated test definitions
*/
public abstract void generateTests(Consumer<SFMGameTestDefinition> testConsumer);
}
package ca.teamdman.sfm.gametest;

import java.util.function.Consumer;

/**
* Base class for game test generators. Subclasses annotated with
* {@link SFMGameTestGenerator} will have their {@link #generateTests} method
* invoked during test discovery to produce multiple game test definitions.
*/
public abstract class SFMGameTestGeneratorBase {

/**
* Generates game test definitions and passes them to the provided consumer.
*
* @param testConsumer a consumer that accepts generated test definitions
*/
public abstract void generateTests(Consumer<SFMGameTestDefinition> testConsumer);
}
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ public void assertExpr(

BoolExpr expr = BoolExpr.from(exprString);
ProgramContext programContext = new ProgramContext(
new Program(new ASTBuilder(), "temp lol", List.of(), Set.of(), Set.of()),
new Program(new ASTBuilder(), "temp lol", List.of(), List.of(), List.of(), Set.of(), Set.of()),
manager,
ExecuteProgramBehaviour::new
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ private void runConditions(
if (conditions.isEmpty()) return;
List<BoolExpr> expressions = conditions.stream().map(BoolExpr::from).toList();
ProgramContext programContext = new ProgramContext(
new Program(new ASTBuilder(), "temp lol", List.of(), Set.of(), Set.of()),
new Program(new ASTBuilder(), "temp lol", List.of(), List.of(), List.of(), Set.of(), Set.of()),
manager,
ExecuteProgramBehaviour::new
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
package ca.teamdman.sfm.gametest.tests.library;

import ca.teamdman.sfm.common.blockentity.LibraryBlockEntity;
import ca.teamdman.sfm.common.blockentity.ManagerBlockEntity;
import ca.teamdman.sfm.common.item.DiskItem;
import ca.teamdman.sfm.common.label.LabelPositionHolder;
import ca.teamdman.sfm.common.registry.registration.SFMBlocks;
import ca.teamdman.sfm.common.registry.registration.SFMItems;
import ca.teamdman.sfm.gametest.SFMGameTest;
import ca.teamdman.sfm.gametest.SFMGameTestDefinition;
import ca.teamdman.sfm.gametest.SFMGameTestHelper;
import net.minecraft.core.BlockPos;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.Blocks;


/**
* Tests chained library imports where multiple libraries depend on each other.
* <p>
* Setup:
* - base_protocols library: defines HasInput and HasOutput protocols
* - struct_lib library: imports base_protocols, defines Furnace struct
* - macro_lib library: imports base_protocols and struct_lib, defines smelt macro
* - Manager: imports all three libraries and uses the smelt macro
* <p>
* This tests that:
* 1. Libraries can import other libraries
* 2. Definitions are properly resolved across the chain
* 3. Protocol constraints work with chained imports
*/
@SuppressWarnings({
"RedundantSuppression",
"DataFlowIssue",
"OptionalGetWithoutIsPresent",
"DuplicatedCode"
})
@SFMGameTest
public class LibraryChainedImportsGameTest extends SFMGameTestDefinition {

@Override
public String template() {
return "7x3x3";
}

@Override
public int maxTicks() {
return 200;
}

@Override
public void run(SFMGameTestHelper helper) {
// Layout (y=2 front row): [ore] - [baseLib] - [structLib] - [Manager] - [macroLib] - [furnace] - [ingots]
// Layout (y=2 back row): [cable] - [cable] - [cable] - [cable] - [cable] - [cable] - [cable]
BlockPos orePos = new BlockPos(0, 2, 0);
BlockPos baseLibPos = new BlockPos(1, 2, 0);
BlockPos structLibPos = new BlockPos(2, 2, 0);
BlockPos managerPos = new BlockPos(3, 2, 0);
BlockPos macroLibPos = new BlockPos(4, 2, 0);
BlockPos furnacePos = new BlockPos(5, 2, 0);
BlockPos ingotsPos = new BlockPos(6, 2, 0);

// Cable row behind to connect everything
BlockPos cable0Pos = new BlockPos(0, 2, 1);
BlockPos cable1Pos = new BlockPos(1, 2, 1);
BlockPos cable2Pos = new BlockPos(2, 2, 1);
BlockPos cable3Pos = new BlockPos(3, 2, 1);
BlockPos cable4Pos = new BlockPos(4, 2, 1);
BlockPos cable5Pos = new BlockPos(5, 2, 1);
BlockPos cable6Pos = new BlockPos(6, 2, 1);

// Place main blocks
helper.setBlock(orePos, SFMBlocks.TEST_BARREL.get());
helper.setBlock(baseLibPos, SFMBlocks.LIBRARY_BLOCK.get());
helper.setBlock(structLibPos, SFMBlocks.LIBRARY_BLOCK.get());
helper.setBlock(managerPos, SFMBlocks.MANAGER.get());
helper.setBlock(macroLibPos, SFMBlocks.LIBRARY_BLOCK.get());
helper.setBlock(furnacePos, SFMBlocks.TEST_BARREL.get());
helper.setBlock(ingotsPos, SFMBlocks.TEST_BARREL.get());

// Place cable row behind
helper.setBlock(cable0Pos, SFMBlocks.CABLE.get());
helper.setBlock(cable1Pos, SFMBlocks.CABLE.get());
helper.setBlock(cable2Pos, SFMBlocks.CABLE.get());
helper.setBlock(cable3Pos, SFMBlocks.CABLE.get());
helper.setBlock(cable4Pos, SFMBlocks.CABLE.get());
helper.setBlock(cable5Pos, SFMBlocks.CABLE.get());
helper.setBlock(cable6Pos, SFMBlocks.CABLE.get());

// Get block entities
LibraryBlockEntity baseLib = (LibraryBlockEntity) helper.getBlockEntity(baseLibPos);
LibraryBlockEntity structLib = (LibraryBlockEntity) helper.getBlockEntity(structLibPos);
LibraryBlockEntity macroLib = (LibraryBlockEntity) helper.getBlockEntity(macroLibPos);
ManagerBlockEntity manager = (ManagerBlockEntity) helper.getBlockEntity(managerPos);

// Create base_protocols library disk
ItemStack baseLibDisk = new ItemStack(SFMItems.DISK.get());
DiskItem.setProgram(baseLibDisk, """
NAME "base_protocols"

protocol HasInput
input: sidequalifier slotqualifier
end

protocol HasOutput
output: sidequalifier slotqualifier
end
""");
baseLib.setItem(0, baseLibDisk);

// Create struct_lib library disk
ItemStack structLibDisk = new ItemStack(SFMItems.DISK.get());
DiskItem.setProgram(structLibDisk, """
NAME "struct_lib"
use library "base_protocols"

struct Furnace : HasInput, HasOutput
input: EACH SIDE SLOTS 0-8
output: EACH SIDE SLOTS 9-17
end
""");
structLib.setItem(0, structLibDisk);

// Create macro_lib library disk
ItemStack macroLibDisk = new ItemStack(SFMItems.DISK.get());
DiskItem.setProgram(macroLibDisk, """
NAME "macro_lib"
use library "base_protocols"
use library "struct_lib"

macro smelt(machine: HasInput, machine2: HasOutput, src, dst)
input from src
output to machine using input
forget
input from machine2 using output
output to dst
end
""");
macroLib.setItem(0, macroLibDisk);

// Create manager disk that imports all libraries
manager.setItem(0, new ItemStack(SFMItems.DISK.get()));
manager.setProgram("""
NAME "Chained Manager"
use library "base_protocols"
use library "struct_lib"
use library "macro_lib"

let furnace = Furnace

every 20 ticks do
DO smelt(furnace, furnace, ore_chest, ingot_chest)
end
""");

// Setup labels
LabelPositionHolder labelHolder = LabelPositionHolder.empty()
.add("ore_chest", helper.absolutePos(orePos))
.add("furnace", helper.absolutePos(furnacePos))
.add("ingot_chest", helper.absolutePos(ingotsPos));
labelHolder.save(manager.getDisk());

// Put ore in source
var oreHandler = helper.getItemHandler(orePos);
oreHandler.insertItem(0, new ItemStack(Blocks.GOLD_ORE, 32), false);

// Put ingots in furnace output (simulating smelting)
var furnaceHandler = helper.getItemHandler(furnacePos);
furnaceHandler.insertItem(9, new ItemStack(Blocks.GOLD_BLOCK, 4), false);

// Wait for libraries to compile and manager to run
helper.runAfterDelay(20, () -> {
// Verify all library disks have no errors
helper.assertTrue(
DiskItem.getErrors(baseLib.getItem(0)).isEmpty(),
"base_protocols library should have no errors but had: " + DiskItem.getErrors(baseLib.getItem(0))
);
helper.assertTrue(
DiskItem.getErrors(structLib.getItem(0)).isEmpty(),
"struct_lib library should have no errors but had: " + DiskItem.getErrors(structLib.getItem(0))
);
helper.assertTrue(
DiskItem.getErrors(macroLib.getItem(0)).isEmpty(),
"macro_lib library should have no errors but had: " + DiskItem.getErrors(macroLib.getItem(0))
);

// Verify manager disk has no errors
helper.assertTrue(
DiskItem.getErrors(manager.getDisk()).isEmpty(),
"Manager disk should have no errors but had: " + DiskItem.getErrors(manager.getDisk())
);

helper.succeedIfManagerDidThingWithoutLagging(manager, () -> {
// Verify ore was moved from source to furnace input
helper.assertTrue(
helper.count(oreHandler, Blocks.GOLD_ORE) == 0,
"Ore chest should be empty but has " + helper.count(oreHandler, Blocks.GOLD_ORE) + " gold ore"
);
helper.assertTrue(
helper.count(furnaceHandler, Blocks.GOLD_ORE) == 32,
"Furnace should have 32 gold ore in input slots but has " + helper.count(furnaceHandler, Blocks.GOLD_ORE)
);

// Verify ingots were moved from furnace output to ingot chest
var ingotsHandler = helper.getItemHandler(ingotsPos);
helper.assertTrue(
helper.count(ingotsHandler, Blocks.GOLD_BLOCK) == 4,
"Ingot chest should have 4 gold blocks but has " + helper.count(ingotsHandler, Blocks.GOLD_BLOCK)
);
helper.assertTrue(
helper.count(furnaceHandler, Blocks.GOLD_BLOCK) == 0,
"Furnace output should be empty but has " + helper.count(furnaceHandler, Blocks.GOLD_BLOCK)
);
});
});
}
}
Loading