diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..b18288c6c --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,93 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "java", + "request": "launch", + "name": "Client", + "presentation": { + "group": "Mod Development - AdvancedPeripherals", + "order": 0 + }, + "projectName": "AdvancedPeripherals", + "mainClass": "net.neoforged.devlaunch.Main", + "args": [ + "@C:\\Users\\Administrator\\Documents\\AdvancedPeripherals\\build\\moddev\\clientRunProgramArgs.txt" + ], + "vmArgs": [ + "@C:\\Users\\Administrator\\Documents\\AdvancedPeripherals\\build\\moddev\\clientRunVmArgs.txt", + "-Dfml.modFolders\u003dadvancedperipherals%%C:\\Users\\Administrator\\Documents\\AdvancedPeripherals\\bin\\main" + ], + "cwd": "${workspaceFolder}\\run", + "env": {}, + "console": "internalConsole", + "shortenCommandLine": "none" + }, + { + "type": "java", + "request": "launch", + "name": "Data", + "presentation": { + "group": "Mod Development - AdvancedPeripherals", + "order": 1 + }, + "projectName": "AdvancedPeripherals", + "mainClass": "net.neoforged.devlaunch.Main", + "args": [ + "@C:\\Users\\Administrator\\Documents\\AdvancedPeripherals\\build\\moddev\\dataRunProgramArgs.txt" + ], + "vmArgs": [ + "@C:\\Users\\Administrator\\Documents\\AdvancedPeripherals\\build\\moddev\\dataRunVmArgs.txt", + "-Dfml.modFolders\u003dadvancedperipherals%%C:\\Users\\Administrator\\Documents\\AdvancedPeripherals\\bin\\main" + ], + "cwd": "${workspaceFolder}\\run", + "env": {}, + "console": "internalConsole", + "shortenCommandLine": "none" + }, + { + "type": "java", + "request": "launch", + "name": "GameTestServer", + "presentation": { + "group": "Mod Development - AdvancedPeripherals", + "order": 2 + }, + "projectName": "AdvancedPeripherals", + "mainClass": "net.neoforged.devlaunch.Main", + "args": [ + "@C:\\Users\\Administrator\\Documents\\AdvancedPeripherals\\build\\moddev\\gameTestServerRunProgramArgs.txt" + ], + "vmArgs": [ + "@C:\\Users\\Administrator\\Documents\\AdvancedPeripherals\\build\\moddev\\gameTestServerRunVmArgs.txt", + "-Dfml.modFolders\u003dadvancedperipherals%%C:\\Users\\Administrator\\Documents\\AdvancedPeripherals\\bin\\main" + ], + "cwd": "${workspaceFolder}\\run", + "env": {}, + "console": "internalConsole", + "shortenCommandLine": "none" + }, + { + "type": "java", + "request": "launch", + "name": "Server", + "presentation": { + "group": "Mod Development - AdvancedPeripherals", + "order": 3 + }, + "projectName": "AdvancedPeripherals", + "mainClass": "net.neoforged.devlaunch.Main", + "args": [ + "@C:\\Users\\Administrator\\Documents\\AdvancedPeripherals\\build\\moddev\\serverRunProgramArgs.txt" + ], + "vmArgs": [ + "@C:\\Users\\Administrator\\Documents\\AdvancedPeripherals\\build\\moddev\\serverRunVmArgs.txt", + "-Dfml.modFolders\u003dadvancedperipherals%%C:\\Users\\Administrator\\Documents\\AdvancedPeripherals\\bin\\main" + ], + "cwd": "${workspaceFolder}\\run", + "env": {}, + "console": "internalConsole", + "shortenCommandLine": "none" + } + ] +} \ No newline at end of file diff --git a/docs/RELEASE.md b/docs/RELEASE.md deleted file mode 100644 index 41bce4134..000000000 --- a/docs/RELEASE.md +++ /dev/null @@ -1,41 +0,0 @@ -## Release process - -This documentation focused on release process for Advanced Peripherals. - -### Preparation - -First, you need to get curseforge api -key. [Here](https://support.curseforge.com/en/support/solutions/articles/9000197321-curseforge-api) is some -documentation about it. - -Then, you need to get modrinth key, from [settings](https://modrinth.com/dashboard/settings) in "Authorization token" -section - -Second, you need to create `.env` file in root folder and put in this file - -``` -CURSEFORGE_KEY= -MODRINTH_KEY= -``` - -If you also want to do manual upload to [AdvancedPeripheral maven repository](https://mvn.intelligence-modding.de/), you can add next variables - -``` -PUBLIC_REP_NAME= -PUBLIC_REP_KEY= -``` - -### Before release - -- Manually review `CHANGELOG.md` to check it for mistakes or some problems -- Make sure, that `mod_version` and `release_type` in `gradle.properties` matches to what you want to receive - -### Release process - -- Execute `./gradlew patchChangelog` or manually update `CHANGELOG.md` and commit result to repository -- ~~Run `git tag -` to create git tag and push it to repository via `git push --tags`~~ -- Upload project to github via `./gradlew githubRelease` -- Upload project to curseforge via `./gradlew curseforge` -- Upload project to modrinth via `./gradlew publishModrinth` -- Upload project to maven via `./gradlew publishAllPublicationsToPublicRepository` -- Update `mod_version` to next patch or minor version, what you want diff --git a/src/main/java/de/srendi/advancedperipherals/common/addons/appliedenergistics/AEApi.java b/src/main/java/de/srendi/advancedperipherals/common/addons/appliedenergistics/AEApi.java index e80880bcb..dc5f1c154 100644 --- a/src/main/java/de/srendi/advancedperipherals/common/addons/appliedenergistics/AEApi.java +++ b/src/main/java/de/srendi/advancedperipherals/common/addons/appliedenergistics/AEApi.java @@ -128,7 +128,7 @@ public static Pair findAEChemicalFromStack(MEStorage monitor, } @NotNull - public static Pair findAEChemicalFromFilter(MEStorage monitor, @Nullable ICraftingService crafting, ChemicalFilter filter) { + public static Pair findAEChemicalFromFilter(MEStorage monitor, @Nullable ICraftingService crafting, ChemicalFilter filter) { for (Object2LongMap.Entry temp : monitor.getAvailableStacks()) { if (temp.getKey() instanceof MekanismKey key && filter.test(key.getStack())) return Pair.of(temp.getLongValue(), key); diff --git a/src/main/java/de/srendi/advancedperipherals/common/addons/appliedenergistics/AECraftJob.java b/src/main/java/de/srendi/advancedperipherals/common/addons/appliedenergistics/AECraftJob.java index 05d57a3a5..948ed1f31 100644 --- a/src/main/java/de/srendi/advancedperipherals/common/addons/appliedenergistics/AECraftJob.java +++ b/src/main/java/de/srendi/advancedperipherals/common/addons/appliedenergistics/AECraftJob.java @@ -260,7 +260,10 @@ public void jobStateChanged() { } if (jobLink.isCanceled() && !isJobCanceled) { - fireEvent(false, StatusConstants.JOB_CANCELED); + // If we already force-marked this job as done, do not emit a canceled event + if (!isJobDone) { + fireEvent(false, StatusConstants.JOB_CANCELED); + } setJobCanceled(); return; } @@ -271,6 +274,17 @@ public void jobStateChanged() { } } + /** + * Manually fire JOB_DONE notification for force completed jobs. + * This bypasses the normal jobLink.isDone() check. + */ + public void forceFireJobDone() { + if (!isJobDone) { + fireEvent(false, StatusConstants.JOB_DONE); + setJobDone(); + } + } + private void prepareCPUAndStatus(ICraftingService service) { if (jobLink == null || jobStatus != null || !startedCrafting) { return; diff --git a/src/main/java/de/srendi/advancedperipherals/common/addons/computercraft/peripheral/InventoryManagerPeripheral.java b/src/main/java/de/srendi/advancedperipherals/common/addons/computercraft/peripheral/InventoryManagerPeripheral.java index 9bc096dd8..a27a8f729 100644 --- a/src/main/java/de/srendi/advancedperipherals/common/addons/computercraft/peripheral/InventoryManagerPeripheral.java +++ b/src/main/java/de/srendi/advancedperipherals/common/addons/computercraft/peripheral/InventoryManagerPeripheral.java @@ -18,8 +18,8 @@ import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ArmorItem; import net.minecraft.world.item.ItemStack; -import net.minecraft.world.level.block.entity.BlockEntity; -import net.neoforged.neoforge.capabilities.BlockCapability; + + import net.neoforged.neoforge.capabilities.Capabilities; import net.neoforged.neoforge.items.IItemHandler; import net.neoforged.neoforge.items.wrapper.PlayerArmorInvWrapper; diff --git a/src/main/java/de/srendi/advancedperipherals/common/addons/computercraft/peripheral/MEBridgePeripheral.java b/src/main/java/de/srendi/advancedperipherals/common/addons/computercraft/peripheral/MEBridgePeripheral.java index 6580dd5a6..7c3a33d38 100644 --- a/src/main/java/de/srendi/advancedperipherals/common/addons/computercraft/peripheral/MEBridgePeripheral.java +++ b/src/main/java/de/srendi/advancedperipherals/common/addons/computercraft/peripheral/MEBridgePeripheral.java @@ -1,13 +1,19 @@ package de.srendi.advancedperipherals.common.addons.computercraft.peripheral; +import de.srendi.advancedperipherals.AdvancedPeripherals; import appeng.api.crafting.IPatternDetails; import appeng.api.networking.IGridNode; import appeng.api.networking.IManagedGridNode; +import appeng.api.networking.crafting.CraftingJobStatus; import appeng.api.networking.crafting.ICraftingCPU; +import appeng.api.networking.crafting.ICraftingLink; import appeng.api.networking.crafting.ICraftingService; import appeng.api.stacks.AEFluidKey; import appeng.api.stacks.AEItemKey; +import appeng.api.stacks.AEKey; +import appeng.api.stacks.GenericStack; import appeng.api.storage.MEStorage; +import appeng.api.config.Actionable; import appeng.crafting.pattern.EncodedPatternItem; import dan200.computercraft.api.lua.IArguments; import dan200.computercraft.api.lua.LuaException; @@ -36,7 +42,16 @@ import de.srendi.advancedperipherals.common.util.inventory.InventoryUtil; import de.srendi.advancedperipherals.common.util.inventory.ItemFilter; import de.srendi.advancedperipherals.lib.peripherals.BasePeripheral; +import de.srendi.advancedperipherals.network.APNetworking; +import de.srendi.advancedperipherals.network.toclient.CraftingCompleteToastPacket; +import de.srendi.advancedperipherals.network.toclient.ToastToClientPacket; +import de.srendi.advancedperipherals.common.util.CoordUtil; import me.ramidzkh.mekae2.ae2.MekanismKey; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.Level; +import net.neoforged.neoforge.server.ServerLifecycleHooks; import net.neoforged.neoforge.fluids.capability.IFluidHandler; import net.neoforged.neoforge.items.IItemHandler; import org.jetbrains.annotations.NotNull; @@ -44,6 +59,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Map; public class MEBridgePeripheral extends BasePeripheral> implements IStorageSystemPeripheral { @@ -828,6 +844,165 @@ public MethodResult cancelCraftingTasks(IArguments arguments) throws LuaExceptio return MethodResult.of(jobsCanceled); } + @Override + @LuaFunction(mainThread = true) + public MethodResult forceCompleteCraftingTasks(IArguments arguments) throws LuaException { + if (!isAvailable()) + return notConnected(0); + + ICraftingService craftingGrid = node.getGrid().getService(ICraftingService.class); + + Pair, String> filter = GenericFilter.parseGeneric(new ObjectLuaTable(arguments.getTable(0))); + if (filter.getRight() != null && !"NO_NAME_OR_TYPE".equals(filter.getRight())) + return MethodResult.of(0, filter.getRight()); + + GenericFilter parsedFilter = filter.getLeft(); + boolean matchAll = parsedFilter.isEmpty(); + + int jobsCompleted = 0; + for (ICraftingCPU cpu : craftingGrid.getCpus()) { + if (cpu.isBusy() && cpu.getJobStatus() != null && (matchAll || parsedFilter.testAE(cpu.getJobStatus().crafting()))) { + try { + CraftingJobStatus jobStatus = cpu.getJobStatus(); + GenericStack craftingStack = jobStatus.crafting(); + AEKey craftedItem = craftingStack.what(); + long totalAmount = craftingStack.amount(); + + AdvancedPeripherals.debug("[DEBUG] ==> Processing job for: " + craftedItem.getDisplayName().getString()); + AdvancedPeripherals.debug("[DEBUG] ==> Total target amount: " + totalAmount); + AdvancedPeripherals.debug("[DEBUG] ==> CPU busy: " + cpu.isBusy()); + AdvancedPeripherals.debug("[DEBUG] ==> CPU type: " + cpu.getClass().getSimpleName()); + + // Calculate the actual completed amount using AE2 internal logic + long actualCraftedAmount = totalAmount; + if (cpu instanceof appeng.me.cluster.implementations.CraftingCPUCluster cpuCluster) { + try { + appeng.crafting.execution.CraftingCpuLogic craftingCpuLogic = cpuCluster.craftingLogic; + long pending = craftingCpuLogic.getPendingOutputs(craftedItem); + long active = craftingCpuLogic.getWaitingFor(craftedItem); + actualCraftedAmount = totalAmount - (pending + active); + + AdvancedPeripherals.debug("[DEBUG] ==> AE2 Logic - Pending: " + pending + ", Active: " + active + ", Calculated Completed: " + actualCraftedAmount); + + // If calculated amount is 0 or negative, it means nothing is completed yet + // In this case, we should force complete with the total requested amount + if (actualCraftedAmount <= 0) { + AdvancedPeripherals.debug("[DEBUG] ==> No items completed yet, using total amount for force completion"); + actualCraftedAmount = totalAmount; + } + } catch (Exception e) { + AdvancedPeripherals.debug("[DEBUG] ==> Could not calculate precise amount: " + e.getMessage()); + e.printStackTrace(); + // Fall back to total amount if calculation fails + actualCraftedAmount = totalAmount; + } + } else { + AdvancedPeripherals.debug("[DEBUG] ==> Non-CraftingCPUCluster, using total amount: " + totalAmount); + } + + // For force completion, we insert the full requested amount + // The notification should show what we actually added to storage + long amountToInsert = actualCraftedAmount; + AdvancedPeripherals.debug("[DEBUG] ==> Final amount to insert and notify: " + amountToInsert); + + MEStorage storage = AEApi.getMonitor(node); + long inserted = storage.insert(craftedItem, amountToInsert, Actionable.MODULATE, bridge); + AdvancedPeripherals.debug("[DEBUG] ==> Actually inserted into storage: " + inserted); + + // Cancel the job without relying on AE2 notification logic + cpu.cancelJob(); + Thread.sleep(50); + boolean completed = !cpu.isBusy(); + AdvancedPeripherals.debug("[DEBUG] ==> CPU idle after cancellation: " + completed); + + if (completed) { + jobsCompleted++; + + AdvancedPeripherals.debug("[DEBUG] ==> Sending notification with amount: " + amountToInsert); + // Send custom notification with crafted item details + sendForceCompletionNotification(jobsCompleted, craftedItem, amountToInsert, inserted); + } + } catch (Exception e) { + // Continue to next CPU if this one fails + continue; + } + } + } + return MethodResult.of(jobsCompleted); + } + + private void sendForceCompletionNotification(int jobNumber, AEKey craftedItem, long craftedAmount, long insertedAmount) { + AdvancedPeripherals.debug("[DEBUG-NOTIF] ==> sendForceCompletionNotification called with:"); + AdvancedPeripherals.debug("[DEBUG-NOTIF] ==> jobNumber: " + jobNumber); + AdvancedPeripherals.debug("[DEBUG-NOTIF] ==> craftedItem: " + craftedItem.getDisplayName().getString()); + AdvancedPeripherals.debug("[DEBUG-NOTIF] ==> craftedAmount: " + craftedAmount); + AdvancedPeripherals.debug("[DEBUG-NOTIF] ==> insertedAmount: " + insertedAmount); + + // Generate unique job ID for this force completion (matching original AE2 system style) + long jobId = System.currentTimeMillis() + jobNumber; + + // Send ComputerCraft notification exactly like original AE2 system + for (IComputerAccess computer : getConnectedComputers()) { + computer.queueEvent("ae_crafting", false, jobId, "JOB_DONE"); + } + + if (shouldSendCraftingNotifications()) { + sendToastToNearbyPlayers(jobId, craftedItem, craftedAmount, insertedAmount); + } else { + AdvancedPeripherals.debug("[DEBUG-NOTIF] ==> Skipping toast notification - notifications disabled"); + } + } + + private void sendToastToNearbyPlayers(long jobId, AEKey craftedItem, long craftedAmount, long insertedAmount) { + AdvancedPeripherals.debug("[DEBUG-TOAST] ==> sendToastToNearbyPlayers called with:"); + AdvancedPeripherals.debug("[DEBUG-TOAST] ==> jobId: " + jobId); + AdvancedPeripherals.debug("[DEBUG-TOAST] ==> craftedItem: " + craftedItem.getDisplayName().getString()); + AdvancedPeripherals.debug("[DEBUG-TOAST] ==> craftedAmount: " + craftedAmount); + AdvancedPeripherals.debug("[DEBUG-TOAST] ==> insertedAmount: " + insertedAmount); + + ResourceKey dimension = getLevel().dimension(); + int range = 32; // Toast notification range + + AdvancedPeripherals.debug("[DEBUG-TOAST] ==> Sending AE2 notification packets to nearby players"); + + for (ServerPlayer player : ServerLifecycleHooks.getCurrentServer().getPlayerList().getPlayers()) { + if (player.level().dimension() != dimension) + continue; + + if (CoordUtil.isInRange(getPos(), getLevel(), player, range, range)) { + AdvancedPeripherals.debug("[DEBUG-TOAST] ==> Sending packet to player: " + player.getName().getString()); + + // Send packet with item data - client will create authentic AE2 FinishedJobToast + Component title = Component.literal("Auto-Crafting Complete"); + Component message = Component.literal(craftedAmount + " " + craftedItem.getDisplayName().getString()); + CraftingCompleteToastPacket packet = new CraftingCompleteToastPacket(title, message, craftedItem, craftedAmount); + APNetworking.sendTo(player, packet); + } + } + } + + /** + * Check if crafting notifications should be sent to players. + * This method checks the server-side configuration. + * Individual client settings are checked on each client when they receive the packet. + * + * @return true if notifications should be sent, false otherwise + */ + private boolean shouldSendCraftingNotifications() { + // Check server-side setting + boolean serverSetting = APConfig.PERIPHERALS_CONFIG.meCraftingNotifications.get(); + + if (!serverSetting) { + AdvancedPeripherals.debug("[DEBUG-NOTIF] ==> Notifications disabled by server config (meCraftingNotifications=false)"); + return false; + } + + // Server setting is enabled, send packets to all clients + // Each client will check their own setting when they receive the packet + AdvancedPeripherals.debug("[DEBUG-NOTIF] ==> Server notifications enabled, sending packets to clients"); + return true; + } + @Override @LuaFunction(mainThread = true) public final MethodResult isCraftable(IArguments arguments) throws LuaException { @@ -881,4 +1056,129 @@ public final MethodResult getCraftingCPUs() { } return MethodResult.of(map); } + + @LuaFunction(mainThread = true) + public final MethodResult forceStopCPU(String cpuName) { + if (!isAvailable()) + return notConnected(null); + + if (cpuName == null || cpuName.trim().isEmpty()) + return MethodResult.of(null, "CPU name cannot be empty"); + + ICraftingCPU cpu = AEApi.getCraftingCPU(node, cpuName.trim()); + if (cpu == null) + return MethodResult.of(null, "CPU not found: " + cpuName); + + if (!cpu.isBusy()) + return MethodResult.of(null, "CPU is not busy: " + cpuName); + + try { + cpu.cancelJob(); + return MethodResult.of(true, "Successfully stopped CPU: " + cpuName); + } catch (Exception e) { + return MethodResult.of(null, "Failed to stop CPU: " + e.getMessage()); + } + } + + @LuaFunction(mainThread = true) + public final MethodResult forceCompleteCPU(String cpuName) { + if (!isAvailable()) + return notConnected(null); + + if (cpuName == null || cpuName.trim().isEmpty()) + return MethodResult.of(null, "CPU name cannot be empty"); + + ICraftingCPU cpu = AEApi.getCraftingCPU(node, cpuName.trim()); + if (cpu == null) + return MethodResult.of(null, "CPU not found: " + cpuName); + + if (!cpu.isBusy()) + return MethodResult.of(null, "CPU is not busy: " + cpuName); + + try { + CraftingJobStatus jobStatus = cpu.getJobStatus(); + if (jobStatus == null) + return MethodResult.of(null, "No active job found on CPU: " + cpuName); + + // Get the item being crafted and its quantity + GenericStack craftingStack = jobStatus.crafting(); + AEKey craftedItem = craftingStack.what(); + long craftedAmount = craftingStack.amount(); + + // Get the ME storage to insert the completed items + MEStorage storage = AEApi.getMonitor(node); + + // Insert the crafted items into storage first + long inserted = storage.insert(craftedItem, craftedAmount, Actionable.MODULATE, bridge); + + // Access the CraftingCPUCluster to force natural job completion + if (cpu instanceof appeng.me.cluster.implementations.CraftingCPUCluster cpuCluster) { + try { + // Get the current job link to trigger proper completion notification + ICraftingLink currentLink = cpuCluster.craftingLogic.getLastLink(); + if (currentLink != null) { + // Force the job to be marked as done by manipulating the link state + // This will trigger the jobStateChange callback in MEBridgeEntity + // which then calls jobStateChanged() on the AECraftJob, firing the completion event + + // Cancel the job to free the CPU and trigger state change + cpuCluster.craftingLogic.cancel(); + + // Small delay to allow the state change to propagate + Thread.sleep(50); + + // The jobStateChange callback should have been triggered automatically by AE2 + // when the job was canceled, which will call jobStateChanged() on matching jobs + + boolean isNowIdle = !cpu.isBusy(); + + if (inserted == craftedAmount && isNowIdle) { + return MethodResult.of(true, "Successfully force completed CPU: " + cpuName + + " (" + inserted + " items added, completion notification sent)"); + } else if (inserted > 0 && isNowIdle) { + return MethodResult.of(true, "Partially completed CPU: " + cpuName + + " (" + inserted + "/" + craftedAmount + " items added, completion notification sent)"); + } else if (isNowIdle) { + return MethodResult.of(true, "Force completed CPU: " + cpuName + + " (job completed, CPU now idle, " + inserted + " items added, completion notification sent)"); + } else { + return MethodResult.of(false, "Failed to complete CPU: " + cpuName + + " (CPU still busy after completion attempt)"); + } + } + } catch (Exception clusterException) { + // Fallback to regular cancellation if cluster method fails + cpu.cancelJob(); + Thread.sleep(50); + boolean isNowIdle = !cpu.isBusy(); + + if (isNowIdle) { + return MethodResult.of(true, "Force completed CPU (fallback): " + cpuName + + " (" + inserted + " items added, job canceled)"); + } else { + return MethodResult.of(false, "Failed to complete CPU: " + cpuName + + " (CPU still busy after fallback cancellation)"); + } + } + } else { + // Fallback for non-CraftingCPUCluster implementations + cpu.cancelJob(); + Thread.sleep(50); + boolean isNowIdle = !cpu.isBusy(); + + if (isNowIdle) { + return MethodResult.of(true, "Force completed CPU (basic): " + cpuName + + " (" + inserted + " items added, job canceled)"); + } else { + return MethodResult.of(false, "Failed to complete CPU: " + cpuName + + " (CPU still busy after cancellation)"); + } + } + } catch (Exception e) { + return MethodResult.of(false, "Error during force completion: " + e.getMessage()); + } + + // This should never be reached, but added for completeness + return MethodResult.of(false, "Unexpected error in force completion"); + } } diff --git a/src/main/java/de/srendi/advancedperipherals/common/addons/computercraft/peripheral/RSBridgePeripheral.java b/src/main/java/de/srendi/advancedperipherals/common/addons/computercraft/peripheral/RSBridgePeripheral.java index 2b027ad3c..d7fc71cd8 100644 --- a/src/main/java/de/srendi/advancedperipherals/common/addons/computercraft/peripheral/RSBridgePeripheral.java +++ b/src/main/java/de/srendi/advancedperipherals/common/addons/computercraft/peripheral/RSBridgePeripheral.java @@ -2,12 +2,16 @@ import com.refinedmods.refinedstorage.api.autocrafting.Pattern; import com.refinedmods.refinedstorage.api.autocrafting.status.TaskStatus; +import com.refinedmods.refinedstorage.api.core.Action; import com.refinedmods.refinedstorage.api.network.Network; import com.refinedmods.refinedstorage.api.network.NetworkComponent; import com.refinedmods.refinedstorage.api.network.autocrafting.AutocraftingNetworkComponent; import com.refinedmods.refinedstorage.api.network.energy.EnergyNetworkComponent; import com.refinedmods.refinedstorage.api.network.impl.node.AbstractNetworkNode; +import com.refinedmods.refinedstorage.api.network.storage.StorageNetworkComponent; import com.refinedmods.refinedstorage.api.resource.ResourceAmount; +import com.refinedmods.refinedstorage.api.resource.ResourceKey; +import com.refinedmods.refinedstorage.api.storage.Actor; import com.refinedmods.refinedstorage.common.support.resource.ItemResource; import com.refinedmods.refinedstorage.mekanism.ChemicalResource; import com.refinedmods.refinedstorage.neoforge.support.resource.VariantUtil; @@ -781,6 +785,57 @@ public final MethodResult cancelCraftingTasks(IArguments arguments) throws LuaEx return MethodResult.of(canceled); } + @Override + @LuaFunction(mainThread = true) + public final MethodResult forceCompleteCraftingTasks(IArguments arguments) throws LuaException { + if (!isAvailable()) + return notConnected(0); + + Pair, String> filter = GenericFilter.parseGeneric(new ObjectLuaTable(arguments.getTable(0))); + if (filter.getRight() != null && !"NO_NAME_OR_TYPE".equals(filter.getRight())) + return MethodResult.of(0, filter.getRight()); + + GenericFilter parsedFilter = filter.getLeft(); + boolean matchAll = parsedFilter.isEmpty(); // If filter is empty, match all jobs + + AutocraftingNetworkComponent craftingManager = getComponent(AutocraftingNetworkComponent.class); + StorageNetworkComponent storage = getNetwork().getComponent(StorageNetworkComponent.class); + int completed = 0; + + for (TaskStatus status : craftingManager.getStatuses()) { + if (matchAll || parsedFilter.testRS(new ResourceAmount(status.info().resource(), 1))) { + try { + // Get task details + long amount = status.info().amount(); + ResourceKey resource = status.info().resource(); + + // Insert the completed items into storage first + long inserted = storage.insert(resource, amount, Action.EXECUTE, Actor.EMPTY); + + // Cancel the task to free up the crafting process and trigger notification + craftingManager.cancel(status.info().id()); + + // Manually trigger JOB_DONE notification for matching jobs + final TaskStatus finalStatus = status; + bridge.getJobs().stream() + .filter(job -> job.getCraftingTask() != null && + job.getCraftingTask().info().id().equals(finalStatus.info().id())) + .forEach(job -> { + // Directly fire JOB_DONE notification bypassing normal completion checks + job.forceFireJobDone(); + }); + + completed++; + } catch (Exception e) { + // Continue to next task if this one fails + continue; + } + } + } + + return MethodResult.of(completed); + } + @Override @LuaFunction(mainThread = true) public final MethodResult isCraftable(IArguments arguments) throws LuaException { @@ -873,4 +928,72 @@ public final MethodResult getPatterns(IArguments arguments) throws LuaException return MethodResult.of(RSApi.parsePattern(pattern.getLeft(), autocrafting)); } + + @LuaFunction(mainThread = true) + public final MethodResult forceCompleteCPU(String taskIdOrResourceName) { + if (!isAvailable()) + return notConnected(null); + + if (taskIdOrResourceName == null || taskIdOrResourceName.trim().isEmpty()) + return MethodResult.of(null, "Task ID or resource name cannot be empty"); + + AutocraftingNetworkComponent craftingManager = getComponent(AutocraftingNetworkComponent.class); + TaskStatus targetTask = null; + + // First try to find by task ID + for (TaskStatus status : craftingManager.getStatuses()) { + if (status.info().id().toString().equals(taskIdOrResourceName.trim())) { + targetTask = status; + break; + } + } + + // If not found by ID, try to find by resource name (basic matching) + if (targetTask == null) { + for (TaskStatus status : craftingManager.getStatuses()) { + String resourceName = status.info().resource().toString(); + if (resourceName.contains(taskIdOrResourceName.trim())) { + targetTask = status; + break; + } + } + } + + if (targetTask == null) + return MethodResult.of(null, "Task not found: " + taskIdOrResourceName); + + try { + // Get task details before canceling + long amount = targetTask.info().amount(); + ResourceKey resource = targetTask.info().resource(); + + // Insert the items into the storage network first + Network network = getNetwork(); + StorageNetworkComponent storage = network.getComponent(StorageNetworkComponent.class); + + // Create a resource amount to insert + ResourceAmount resourceAmount = new ResourceAmount(resource, amount); + + // Try to insert the completed items + long inserted = storage.insert(resource, amount, Action.EXECUTE, Actor.EMPTY); + + // Cancel the task to free up the crafting process + craftingManager.cancel(targetTask.info().id()); + + // Manually trigger JOB_DONE notification for matching jobs + final TaskStatus finalTargetTask = targetTask; + bridge.getJobs().stream() + .filter(job -> job.getCraftingTask() != null && + job.getCraftingTask().info().id().equals(finalTargetTask.info().id())) + .forEach(job -> { + // Directly fire JOB_DONE notification bypassing normal completion checks + job.forceFireJobDone(); + }); + + return MethodResult.of(true, "Successfully force completed task: " + taskIdOrResourceName + + " (" + inserted + "/" + amount + " items added, completion notification sent)"); + } catch (Exception e) { + return MethodResult.of(false, "Error during force completion: " + e.getMessage()); + } + } } diff --git a/src/main/java/de/srendi/advancedperipherals/common/addons/refinedstorage/RSCraftJob.java b/src/main/java/de/srendi/advancedperipherals/common/addons/refinedstorage/RSCraftJob.java index 113708b00..61294b9dd 100644 --- a/src/main/java/de/srendi/advancedperipherals/common/addons/refinedstorage/RSCraftJob.java +++ b/src/main/java/de/srendi/advancedperipherals/common/addons/refinedstorage/RSCraftJob.java @@ -128,8 +128,6 @@ public boolean cancelImpl() { @Override public void tick() { super.tick(); - // RS2 does re-create a new instance of the TaskStatus when something changes - so we actually need to re fetch that from the system - // It's cursed, but the only way currently if (craftingTask != null) { for (TaskStatus status : autocraftingComponent.getStatuses()) { if (status.info().id().equals(craftingTask.info().id())) { @@ -153,8 +151,6 @@ public void tick() { } if (optionalId.isEmpty()) { - // This indicates that the second calculation was not successful. Well I guess. There is no java doc and I currently - // don't get an answer from the maintainer. So maybe we want to fire the crafting event return; } @@ -162,7 +158,6 @@ public void tick() { for (TaskStatus status : autocraftingComponent.getStatuses()) { if (status.info().id().equals(id)) { this.craftingTask = status; - // And only now we set that the crafting is started. setStartedCrafting(); break; } @@ -185,7 +180,6 @@ protected void maybeCraft() { return; } - // TODO: I currently don't exactly know when the optional can be empty after the future is done. So I need to evaluate this. if (optionalPreview.isEmpty()) { AdvancedPeripherals.debug("preview optional is empty.", org.apache.logging.log4j.Level.ERROR); fireEvent(true, StatusConstants.UNKNOWN_ERROR); @@ -209,8 +203,6 @@ protected void maybeCraft() { return; } - // How RS2 handles crafting is a bit cursed. We first create a preview which calculates the recipes, and then we check if the preview was successful - // If it was, we again start a task which again calculates the recipes, and then we hope nothing changed from the first calculation futureTask = autocraftingComponent.startTask(toCraft, amount, Actor.EMPTY, false, CancellationToken.NONE); } @@ -248,4 +240,11 @@ public void jobStateChanged() { setJobDone(); } } + + public void forceFireJobDone() { + if (!isJobDone) { + fireEvent(false, StatusConstants.JOB_DONE); + setJobDone(); + } + } } diff --git a/src/main/java/de/srendi/advancedperipherals/common/configuration/APConfig.java b/src/main/java/de/srendi/advancedperipherals/common/configuration/APConfig.java index 3ae818259..323281495 100644 --- a/src/main/java/de/srendi/advancedperipherals/common/configuration/APConfig.java +++ b/src/main/java/de/srendi/advancedperipherals/common/configuration/APConfig.java @@ -16,6 +16,7 @@ public class APConfig { public static final PeripheralsConfig PERIPHERALS_CONFIG = new PeripheralsConfig(); public static final MetaphysicsConfig METAPHYSICS_CONFIG = new MetaphysicsConfig(); public static final WorldConfig WORLD_CONFIG = new WorldConfig(); + public static final ClientConfig CLIENT_CONFIG = new ClientConfig(); public APConfig() { } @@ -29,6 +30,7 @@ public static void register(ModLoadingContext context) { APConfigHelper.registerConfig(KNOWN_CONFIGS, modContainer, PERIPHERALS_CONFIG); APConfigHelper.registerConfig(KNOWN_CONFIGS, modContainer, METAPHYSICS_CONFIG); APConfigHelper.registerConfig(KNOWN_CONFIGS, modContainer, WORLD_CONFIG); + APConfigHelper.registerConfig(KNOWN_CONFIGS, modContainer, CLIENT_CONFIG); } public static class APConfigHelper { diff --git a/src/main/java/de/srendi/advancedperipherals/common/configuration/ClientConfig.java b/src/main/java/de/srendi/advancedperipherals/common/configuration/ClientConfig.java new file mode 100644 index 000000000..93a02480e --- /dev/null +++ b/src/main/java/de/srendi/advancedperipherals/common/configuration/ClientConfig.java @@ -0,0 +1,49 @@ +package de.srendi.advancedperipherals.common.configuration; + +import net.minecraft.FieldsAreNonnullByDefault; +import net.neoforged.fml.config.ModConfig; +import net.neoforged.neoforge.common.ModConfigSpec; + +@FieldsAreNonnullByDefault +public class ClientConfig implements IAPConfig { + + // ME Bridge Client Settings + public final ModConfigSpec.BooleanValue meCraftingNotifications; + + private final ModConfigSpec configSpec; + + public ClientConfig() { + ModConfigSpec.Builder builder = new ModConfigSpec.Builder(); + + builder.comment("Client-side configuration for AdvancedPeripherals").push("Client"); + + builder.push("MEBridge"); + + meCraftingNotifications = builder + .comment("Enable visual toast notifications when forceCompleteCraftingTasks() completes jobs.", + "This is a per-player client setting that works alongside the server setting.", + "Both the server setting AND this client setting must be enabled for notifications to appear.", + "ComputerCraft events are always sent regardless of this setting.") + .define("meCraftingNotifications", true); + + builder.pop(); // MEBridge + builder.pop(); // Client + + configSpec = builder.build(); + } + + @Override + public ModConfigSpec getConfigSpec() { + return configSpec; + } + + @Override + public String getFileName() { + return "client"; + } + + @Override + public ModConfig.Type getType() { + return ModConfig.Type.CLIENT; + } +} \ No newline at end of file diff --git a/src/main/java/de/srendi/advancedperipherals/common/configuration/PeripheralsConfig.java b/src/main/java/de/srendi/advancedperipherals/common/configuration/PeripheralsConfig.java index 62a061140..d24fee6bb 100644 --- a/src/main/java/de/srendi/advancedperipherals/common/configuration/PeripheralsConfig.java +++ b/src/main/java/de/srendi/advancedperipherals/common/configuration/PeripheralsConfig.java @@ -53,6 +53,7 @@ public class PeripheralsConfig implements IAPConfig { // ME Bridge public final ModConfigSpec.BooleanValue enableMEBridge; public final ModConfigSpec.IntValue meConsumption; + public final ModConfigSpec.BooleanValue meCraftingNotifications; // Rs Bridge public final ModConfigSpec.BooleanValue enableRSBridge; @@ -157,6 +158,7 @@ public PeripheralsConfig() { enableMEBridge = builder.comment("Enable the Me Bridge or not.").define("enableMeBridge", true); meConsumption = builder.comment("Power consumption per tick.").defineInRange("mePowerConsumption", 10, 0, Integer.MAX_VALUE); + meCraftingNotifications = builder.comment("Enable crafting completion notifications for force-completed jobs. When enabled, players receive visual toast notifications when forceCompleteCraftingTasks() completes jobs. This works in conjunction with AE2's wireless terminal notification setting - both the server setting AND AE2's client notification setting must be enabled for notifications to appear.").define("meCraftingNotifications", true); pop("RS_Bridge", builder); enableRSBridge = builder.comment("Enable the Rs Bridge or not.").define("enableRsBridge", true); diff --git a/src/main/java/de/srendi/advancedperipherals/common/util/inventory/IStorageSystemPeripheral.java b/src/main/java/de/srendi/advancedperipherals/common/util/inventory/IStorageSystemPeripheral.java index f6fb55e70..8c2f01d0f 100644 --- a/src/main/java/de/srendi/advancedperipherals/common/util/inventory/IStorageSystemPeripheral.java +++ b/src/main/java/de/srendi/advancedperipherals/common/util/inventory/IStorageSystemPeripheral.java @@ -110,6 +110,8 @@ public interface IStorageSystemPeripheral { MethodResult cancelCraftingTasks(IArguments arguments) throws LuaException; + MethodResult forceCompleteCraftingTasks(IArguments arguments) throws LuaException; + MethodResult craftItem(IComputerAccess computer, IArguments arguments) throws LuaException; MethodResult craftFluid(IComputerAccess computer, IArguments arguments) throws LuaException; diff --git a/src/main/java/de/srendi/advancedperipherals/network/APNetworking.java b/src/main/java/de/srendi/advancedperipherals/network/APNetworking.java index fbf09e5dc..5b2199b83 100644 --- a/src/main/java/de/srendi/advancedperipherals/network/APNetworking.java +++ b/src/main/java/de/srendi/advancedperipherals/network/APNetworking.java @@ -1,6 +1,7 @@ package de.srendi.advancedperipherals.network; import de.srendi.advancedperipherals.AdvancedPeripherals; +import de.srendi.advancedperipherals.network.toclient.CraftingCompleteToastPacket; import de.srendi.advancedperipherals.network.toclient.ToastToClientPacket; import de.srendi.advancedperipherals.network.toclient.UsernameToCachePacket; import de.srendi.advancedperipherals.network.toserver.RetrieveUsernamePacket; @@ -20,6 +21,7 @@ public class APNetworking { public static void init(PayloadRegistrar registrar) { registrar.playToClient(ToastToClientPacket.TYPE, ToastToClientPacket.CODEC, ToastToClientPacket::handle); + registrar.playToClient(CraftingCompleteToastPacket.TYPE, CraftingCompleteToastPacket.CODEC, CraftingCompleteToastPacket::handle); registrar.playToClient(UsernameToCachePacket.TYPE, UsernameToCachePacket.CODEC, UsernameToCachePacket::handle); registrar.playToServer(RetrieveUsernamePacket.TYPE, RetrieveUsernamePacket.CODEC, RetrieveUsernamePacket::handle); diff --git a/src/main/java/de/srendi/advancedperipherals/network/toclient/CraftingCompleteToastPacket.java b/src/main/java/de/srendi/advancedperipherals/network/toclient/CraftingCompleteToastPacket.java new file mode 100644 index 000000000..df1445b11 --- /dev/null +++ b/src/main/java/de/srendi/advancedperipherals/network/toclient/CraftingCompleteToastPacket.java @@ -0,0 +1,177 @@ +package de.srendi.advancedperipherals.network.toclient; + +import de.srendi.advancedperipherals.AdvancedPeripherals; +import de.srendi.advancedperipherals.network.IAPPacket; +import net.minecraft.client.Minecraft; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.ComponentSerialization; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.world.item.ItemStack; +import net.neoforged.fml.loading.FMLEnvironment; +import net.neoforged.neoforge.network.handling.IPayloadContext; +import org.jetbrains.annotations.NotNull; +import appeng.api.stacks.AEKey; +import appeng.api.stacks.AEItemKey; +// Try to import AE2's config classes +import appeng.core.AEConfig; + +public class CraftingCompleteToastPacket implements IAPPacket { + public static final StreamCodec CODEC = StreamCodec.of( + (buffer, value) -> value.write(buffer), + CraftingCompleteToastPacket::decode); + + public static final Type TYPE = new Type<>(AdvancedPeripherals.getRL("craftingcompletetoast")); + + private final Component title; + private final Component message; + private final ItemStack itemStack; + private final long craftedAmount; + + public CraftingCompleteToastPacket(Component title, Component message, AEKey craftedItem, long amount) { + this.title = title; + this.message = message; + this.craftedAmount = amount; + + // Convert AEKey to ItemStack for serialization (but preserve amount separately) + if (craftedItem instanceof AEItemKey itemKey) { + this.itemStack = itemKey.toStack(); + } else { + // Fallback for non-item keys (fluids, etc.) + this.itemStack = ItemStack.EMPTY; + } + } + + public static CraftingCompleteToastPacket decode(RegistryFriendlyByteBuf buffer) { + Component title = ComponentSerialization.STREAM_CODEC.decode(buffer); + Component message = ComponentSerialization.STREAM_CODEC.decode(buffer); + ItemStack itemStack = ItemStack.STREAM_CODEC.decode(buffer); + long craftedAmount = buffer.readLong(); + return new CraftingCompleteToastPacket(title, message, itemStack, craftedAmount); + } + + public void write(RegistryFriendlyByteBuf buffer) { + ComponentSerialization.STREAM_CODEC.encode(buffer, title); + ComponentSerialization.STREAM_CODEC.encode(buffer, message); + ItemStack.STREAM_CODEC.encode(buffer, itemStack); + buffer.writeLong(craftedAmount); + } + + private CraftingCompleteToastPacket(Component title, Component message, ItemStack itemStack, long craftedAmount) { + this.title = title; + this.message = message; + this.itemStack = itemStack; + this.craftedAmount = craftedAmount; + } + + @Override + public void handle(@NotNull IPayloadContext context) { + // Should in the theory not happen, but safe is safe. + if (!FMLEnvironment.dist.isClient()) { + AdvancedPeripherals.debug("Tried to display crafting complete toast on the server, aborting."); + return; + } + + // Check BOTH AdvancedPeripherals AND AE2's notification settings + // This ensures we respect both the mod's setting and AE2's wireless terminal notify button + boolean clientNotificationSetting = de.srendi.advancedperipherals.common.configuration.APConfig.CLIENT_CONFIG.meCraftingNotifications.get(); + + // Access AE2's notification setting using reflection (compatible across AE2 versions) + boolean ae2NotificationSetting = true; // Default to true if we can't access AE2's setting + try { + // Attempt to get AE2's notification setting + // Let's try different potential methods + AdvancedPeripherals.debug("[DEBUG-CLIENT] ==> Attempting to access AE2 config..."); + + // Try AEConfig instance methods + if (AEConfig.instance() != null) { + AdvancedPeripherals.debug("[DEBUG-CLIENT] ==> AEConfig instance found"); + + // Use reflection to access AE2's notification setting + // This is more robust than compile-time method calls + try { + Object aeConfigInstance = AEConfig.instance(); + Class aeConfigClass = aeConfigInstance.getClass(); + + // Try different potential method names using reflection + String[] methodNames = { + "isNotifyForFinishedCraftingJobsEnabled", + "notifyForFinishedCraftingJobs", + "getNotifyForFinishedCraftingJobs", + "isNotifyForFinishedCraftingJobs", + "getNotifyForCompletedCraftingJobs", + "isNotifyForCompletedCraftingJobs" + }; + + boolean methodFound = false; + for (String methodName : methodNames) { + try { + java.lang.reflect.Method method = aeConfigClass.getMethod(methodName); + Object result = method.invoke(aeConfigInstance); + if (result instanceof Boolean) { + ae2NotificationSetting = (Boolean) result; + methodFound = true; + AdvancedPeripherals.debug("[DEBUG-CLIENT] ==> Found AE2 method: " + methodName + " = " + ae2NotificationSetting); + break; + } + } catch (Exception ignored) { + // Try next method + } + } + + if (!methodFound) { + AdvancedPeripherals.debug("[DEBUG-CLIENT] ==> No AE2 notification method found, using default"); + ae2NotificationSetting = true; + } + + } catch (Exception reflectionError) { + AdvancedPeripherals.debug("[DEBUG-CLIENT] ==> Reflection failed: " + reflectionError.getMessage()); + ae2NotificationSetting = true; // Default to enabled + } + + AdvancedPeripherals.debug("[DEBUG-CLIENT] ==> AE2 notification setting: " + ae2NotificationSetting); + } + + // Additional debug info about AE2 integration + AdvancedPeripherals.debug("[DEBUG-CLIENT] ==> AE2 integration complete"); + + } catch (Exception e) { + AdvancedPeripherals.debug("[DEBUG-CLIENT] ==> Could not access AE2 config: " + e.getMessage()); + ae2NotificationSetting = true; // Default to enabled if we can't check + } + + if (!clientNotificationSetting) { + AdvancedPeripherals.debug("[DEBUG-CLIENT] ==> AdvancedPeripherals client notification setting disabled, not showing toast"); + return; + } + + if (!ae2NotificationSetting) { + AdvancedPeripherals.debug("[DEBUG-CLIENT] ==> AE2 notification setting disabled, not showing toast"); + return; + } + + AdvancedPeripherals.debug("[DEBUG-CLIENT] ==> Both notification settings enabled, showing toast"); + + if (!itemStack.isEmpty()) { + AEItemKey aeKey = AEItemKey.of(itemStack); + long amount = this.craftedAmount; + + AdvancedPeripherals.debug("[DEBUG-CLIENT] ==> Creating AE2 FinishedJobToast with amount: " + amount); + + // Create and add AE2's authentic FinishedJobToast + appeng.client.gui.me.common.FinishedJobToast finishedJobToast = + new appeng.client.gui.me.common.FinishedJobToast(aeKey, amount); + + Minecraft.getInstance().getToasts().addToast(finishedJobToast); + AdvancedPeripherals.debug("[DEBUG-CLIENT] ==> AE2 FinishedJobToast displayed successfully"); + } else { + AdvancedPeripherals.debug("[DEBUG-CLIENT] ==> ItemStack is empty, skipping toast"); + } + } + + @NotNull + @Override + public Type type() { + return TYPE; + } +} \ No newline at end of file