Showing preview only (2,233K chars total). Download the full file or copy to clipboard to get everything.
Repository: natanfudge/Not-Enough-Crashes
Branch: 1.21.10
Commit: a79505f388d0
Files: 112
Total size: 2.0 MB
Directory structure:
gitextract_0yxij5oi/
├── .github/
│ └── ISSUE_TEMPLATE/
│ └── bug_report.md
├── .gitignore
├── Configuring Not Enough Crashes.md
├── LICENSE
├── README.md
├── TESTING.md
├── TestFabricMod/
│ ├── build.gradle
│ └── src/
│ └── main/
│ ├── java/
│ │ └── io/
│ │ └── github/
│ │ └── natanfudge/
│ │ └── nectest/
│ │ ├── NecTestCrash.java
│ │ ├── NecTestMod.java
│ │ ├── NecTestModClient.java
│ │ ├── TestSuppressedCloseable.java
│ │ └── mixin/
│ │ ├── ExampleMixin.java
│ │ └── MixinMinecraftServer.java
│ └── resources/
│ ├── fabric.mod.json
│ └── nec_testmod.mixins.json
├── TestForgeMod/
│ ├── build.gradle
│ ├── gradle.properties
│ └── src/
│ └── main/
│ ├── java/
│ │ └── io/
│ │ └── github/
│ │ └── natanfudge/
│ │ └── nectest/
│ │ ├── NecTestCrash.java
│ │ ├── NecTestMod.java
│ │ ├── NecTestModClient.java
│ │ └── mixin/
│ │ ├── ExampleMixin.java
│ │ └── MixinMinecraftServer.java
│ └── resources/
│ ├── META-INF/
│ │ └── mods.toml
│ ├── nec_testmod.mixins.json
│ └── pack.mcmeta
├── TooManyCrashes.license.md
├── build.gradle
├── changelog.md
├── common/
│ ├── build.gradle
│ └── src/
│ └── main/
│ ├── java/
│ │ └── fudge/
│ │ └── notenoughcrashes/
│ │ ├── NotEnoughCrashes.java
│ │ ├── StateManager.java
│ │ ├── api/
│ │ │ └── NotEnoughCrashesApi.java
│ │ ├── config/
│ │ │ ├── ButtonEntry.java
│ │ │ ├── EntryInfo.java
│ │ │ ├── MidnightConfig.java
│ │ │ ├── MidnightConfigListWidget.java
│ │ │ ├── MidnightConfigScreen.java
│ │ │ ├── MidnightSliderWidget.java
│ │ │ ├── NecConfig.java
│ │ │ ├── NecMidnightConfig.java
│ │ │ └── OldNecConfig.java
│ │ ├── gui/
│ │ │ ├── CrashScreen.java
│ │ │ ├── InitErrorScreen.java
│ │ │ └── ProblemScreen.java
│ │ ├── mixinhandlers/
│ │ │ ├── EntryPointCatcher.java
│ │ │ └── InGameCatcher.java
│ │ ├── mixins/
│ │ │ ├── MixinCrashReport.java
│ │ │ ├── MixinTileEntity.java
│ │ │ └── client/
│ │ │ ├── MixinKeyboard.java
│ │ │ ├── MixinMinecraftClient.java
│ │ │ ├── MixinMinecraftServer.java
│ │ │ └── MixinMinecraftServerClientOnly.java
│ │ ├── patches/
│ │ │ └── MinecraftClientAccess.java
│ │ ├── platform/
│ │ │ ├── CommonModMetadata.java
│ │ │ ├── ModsByLocation.java
│ │ │ ├── NecPlatform.java
│ │ │ └── NecPlatformStorage.java
│ │ ├── stacktrace/
│ │ │ ├── CrashUtils.java
│ │ │ └── ModIdentifier.java
│ │ ├── upload/
│ │ │ ├── CrashyUpload.java
│ │ │ ├── LegacyCrashLogUpload.java
│ │ │ └── UploadToCrashyError.java
│ │ └── utils/
│ │ ├── GlUtil.java
│ │ ├── NecLocalization.java
│ │ ├── SystemExitBlock.java
│ │ └── SystemExitBlockedException.java
│ └── resources/
│ ├── assets/
│ │ └── notenoughcrashes/
│ │ └── lang/
│ │ ├── de_de.json
│ │ ├── en_us.json
│ │ ├── et_ee.json
│ │ ├── fr_fr.json
│ │ ├── ja_jp.json
│ │ ├── ms_my.json
│ │ ├── pt_br.json
│ │ ├── ro_ro.json
│ │ ├── ru_ru.json
│ │ ├── tr_tr.json
│ │ ├── uk_ua.json
│ │ ├── zh_cn.json
│ │ ├── zh_hk.json
│ │ ├── zh_tw.json
│ │ └── zlm_arab.json
│ └── notenoughcrashes.mixins.json
├── fabric/
│ ├── build.gradle
│ └── src/
│ └── main/
│ ├── java/
│ │ └── fudge/
│ │ └── notenoughcrashes/
│ │ └── fabric/
│ │ ├── NotEnoughCrashesFabric.java
│ │ ├── config/
│ │ │ └── ModMenuConfigIntegration.java
│ │ ├── mixinhandlers/
│ │ │ └── ModLoaders.java
│ │ ├── mixins/
│ │ │ ├── MixinMain.java
│ │ │ └── client/
│ │ │ ├── CatchInitMinecraftClientMixin.java
│ │ │ └── MixinMain.java
│ │ └── platform/
│ │ └── FabricPlatform.java
│ └── resources/
│ ├── fabric.mod.json
│ ├── notenoughcrashes.accessWidener
│ ├── notenoughcrashes.fabric.mixins.json
│ └── quilt.mod.json
├── gradle/
│ ├── libs.versions.toml
│ └── wrapper/
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── neoforge/
│ ├── build.gradle
│ ├── gradle.properties
│ └── src/
│ └── main/
│ ├── java/
│ │ └── fudge/
│ │ └── notenoughcrashes/
│ │ └── forge/
│ │ ├── NotEnoughCrashesForge.java
│ │ ├── client/
│ │ │ └── NotEnoughCrashesForgeClient.java
│ │ ├── mixins/
│ │ │ ├── MixinMain.java
│ │ │ └── client/
│ │ │ └── MixinMain.java
│ │ └── platform/
│ │ └── ForgePlatform.java
│ └── resources/
│ ├── META-INF/
│ │ └── neoforge.mods.toml
│ ├── notenoughcrashes.forge.mixins.json
│ └── pack.mcmeta
├── pics/
│ └── logo 4.pdn
└── settings.gradle
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Crash Log:**
Link to the crash log, if relevant. You can upload a crash log by using [crashy](https://crashy.net/).
**Game Instance:**
Link to the curseforge/multimc game instance, or to a zip containing your game instance. For most issues, you must provide _something_ including the mods, saves, and configs you have used.
================================================
FILE: .gitignore
================================================
build/
*.ipr
run/
*.iws
out/
*.iml
.gradle/
output/
bin/
libs/
!run/mods/forge-template-loom.jar
.classpath
.project
.idea/
classes/
.metadata
.vscode
.settings
*.launch
.architectury-transformer/
================================================
FILE: Configuring Not Enough Crashes.md
================================================
# Configuring Not Enough Crashes
1. Open your minecraft instance at `C:\Users\<your-name>\AppData\Roaming\.minecraft`
- Multimc and Curse launcher use different paths. Use the launcher GUI to open it.
2. Open `config`.

3. Open `notenoughcrashes.json`.

4. Edit the configuration as you wish. For example, to enable `forceCrashScreen`, edit this text:

To this text:

5. Save the file.
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2021 Fudge and NEC contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# Not Enough Crashes
Discord: [](https://discord.gg/CFaCu97)
Fabric download: [](https://curseforge.com/minecraft/mc-mods/not-enough-crashes)
Forge download: [](https://curseforge.com/minecraft/mc-mods/not-enough-crashes-forge)
Not Enough Crashes improves crashes in Minecraft significantly. For example, it returns the user to the title screen when crashing, instead of closing the game.
Features:
- When crashing, you can go back to the title screen and keep playing, without needing to restart.
- A convenient way to submit syntax-highlighted crash reports via a special crash screen.
- Display a list of mods that were involved in the crash, and can be clicked to go to their issue tracker.
- More useful stack traces that are deobfuscated and include additional information such as NBT for mod developers.
- Force crash logs to always appear (forceCrashScreen). .
# Not Enough Crashes API
Not Enough Crashes stopped providing an API for resetting state when the game crashes because of lack of usage. If you need it back, open an issue.
================================================
FILE: TESTING.md
================================================
# Testing Not Enough Crashes
This documents details how to test Not Enough Crashes after making changes.
- To test Fabric in dev, uncomment `runtimeOnly project(path: ":TestFabricMod", configuration: "namedElements")`
in `fabric/build.gradle`
- To test Forge in dev, uncomment `runtimeOnly project(path: ":TestFabricMod", configuration: "namedElements")`
in `forge/build.gradle`
To test in production, build the test Fabric/Forge mod and add them alongside NEC.
## Crash Handling
### Client Initialization Crashes [Fabric-Only]
- In the game config folder, have a file named `nec_test_mode.txt` with the content `init_crash`.
- Start the game
- Verify:
- The game crashes.
- The crash screen appears.
- 'Not Enough Crashes Test Mod' is blamed.
- In the terminal, no information is repeated.
- Click 'Get link' and verify in the log:
- No information is repeated.
- System details are present.
- Suspected Mods: Not Enough Crashes Test Mod (nec_testmod) (may contain ignored mods)
- Click 'Quit Game' and verify the game closes without any exceptions being logged.
### Initialization Suppressed Exception [Fabric-Only]
- In the game config folder, have a file named `nec_test_mode.txt` with the content `suppressed_crash`.
- Start the game
- Verify:
- The 'Test Main Exception' exception is present in the log as a normal exception.
- The 'Test Suppressed Exception' exception is present in the log as a suppressed exception.
- Click 'Quit Game' and verify the game closes without any exceptions being logged.
### Integrated Server Crashes
- In the game config folder, have a file named `nec_test_mode.txt` with the content `server_crash`.
` - Open a Minecraft World.
` - Verify:
- The game crashes.
- The crash screen appears.
- "Not Enough Crashes Test Mod" is blamed.
- In the terminal, no information is repeated.
- Verify in the **Log**, **TXT File**, and **Get Link Site**:
- No information is repeated.
- System details are present.
- Suspected Mods: Not Enough Crashes Test Mod (nec_testmod) (may contain ignored mods)
- Client Crashes Since Restart: 0
- Integrated Server Crashes Since Restart: 1
- Verify the txt file has a -server ending
- Click 'Back to title screen' and re-enter the world.
- Verify the game is working fine.
### Dedicated Server Crashes [Can be tested in dev only]
- In the game config folder, have a file named `nec_test_mode.txt` with the content `server_crash`.
- Start the server
- Verify:
- The game crashes.
- In the terminal, no information is repeated.
- Verify a -server crash report is generated with:
- Suspected mods: Not Enough Crashes Test Mod (nec_testmod) (may contain ignored mods)
- No information is repeated.
- System details are present.
### Client Reported Crashes
- In the game config folder, have the file named `nec_test_mode.txt` be empty or non-existent.
- Open a Minecraft world.
- Press the left square bracket key.
- Verify:
- The game crashes.
- The crash screen appears.
- 'Not Enough Crashes Test Mod' is blamed (and maybe fabric lifecycle events).
- Click 'Get link' and verify in the log:
- No information is repeated.
- System details are present.
- Suspected Mods: Not Enough Crashes Test Mod (nec_testmod) (may contain ignored mods)
- Client Crashes Since Restart: 1
- Integrated Server Crashes Since Restart: 0
- Click 'Back to title screen' and re-enter the world.
- Verify the game is working fine.
### Client Unreported Crashes
================================================
FILE: TestFabricMod/build.gradle
================================================
import org.gradle.jvm.toolchain.JavaLanguageVersion
ext.fabric_version = libs.versions.fabric.api.get()
archivesBaseName = "nec_testmod"
version = "1.0.0"
group = "io.github.natanfudge"
dependencies {
modImplementation libs.fabric.loader
// Fabric API. This is technically optional, but you probably want it anyway.
modImplementation include( fabricApi.module("fabric-lifecycle-events-v1",fabric_version))
modImplementation include( fabricApi.module("fabric-key-binding-api-v1",fabric_version))
modImplementation include( fabricApi.module("fabric-api-base",fabric_version))
// include("net.fabricmc.fabric-api:fabric-api:${project.fabric_version}")
}
processResources {
inputs.property "version", project.version
filesMatching("fabric.mod.json") {
expand "version": project.version
}
}
tasks.withType(JavaCompile).configureEach {
// ensure that the encoding is set to UTF-8, no matter what the system default is
// this fixes some edge cases with special characters not displaying correctly
// see http://yodaconditions.net/blog/fix-for-java-file-encoding-problems-with-gradle.html
// If Javadoc is generated, this must be specified in that task too.
it.options.encoding = "UTF-8"
// Minecraft 1.17 (21w19a) upwards uses Java 16.
}
java {
// Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task
// if it is present.
// If you remove this line, sources will not be generated.
withSourcesJar()
}
jar {
from("LICENSE") {
rename { "${it}_${project.archivesBaseName}"}
}
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(21))
}
withSourcesJar()
}
================================================
FILE: TestFabricMod/src/main/java/io/github/natanfudge/nectest/NecTestCrash.java
================================================
package io.github.natanfudge.nectest;
public class NecTestCrash extends RuntimeException{
public NecTestCrash(String message){
super(message);
}
}
================================================
FILE: TestFabricMod/src/main/java/io/github/natanfudge/nectest/NecTestMod.java
================================================
package io.github.natanfudge.nectest;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.loader.api.FabricLoader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class NecTestMod implements ModInitializer {
public static String getTestMode() {
Path configDig = FabricLoader.getInstance().getConfigDir();
Path testModePath = configDig.resolve("nec_test_mode.txt");
try {
Files.createDirectories(testModePath.getParent());
if (!Files.exists(testModePath)) {
Files.createFile(testModePath);
}
return new String(Files.readAllBytes(testModePath)).trim();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void onInitialize() {
System.out.println("Test crash mod initializing with testMode=" + getTestMode());
if (getTestMode().equals("init_crash")) {
throw new NecTestCrash("Test Init Crash");
}
if (getTestMode().equals("suppressed_crash")) {
try (TestSuppressedCloseable ignored = new TestSuppressedCloseable()) {
throw new NecTestCrash("Test Main Exception");
}
}
}
}
================================================
FILE: TestFabricMod/src/main/java/io/github/natanfudge/nectest/NecTestModClient.java
================================================
package io.github.natanfudge.nectest;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper;
import net.minecraft.client.option.KeyBinding;
import net.minecraft.client.util.InputUtil;
import org.lwjgl.glfw.GLFW;
public class NecTestModClient implements ClientModInitializer {
private static final KeyBinding tickKeyBinding = KeyBindingHelper.registerKeyBinding(new KeyBinding(
"key.nec_test.crash", // The translation key of the keybinding's name
InputUtil.Type.KEYSYM, // The type of the keybinding, KEYSYM for keyboard, MOUSE for mouse.
GLFW.GLFW_KEY_LEFT_BRACKET, // The keycode of the key
"category.nec_test" // The translation key of the keybinding's category.
));
private static final KeyBinding localeKeyBinding = KeyBindingHelper.registerKeyBinding(new KeyBinding(
"key.nec_test.crash_locale", // The translation key of the keybinding's name
InputUtil.Type.KEYSYM, // The type of the keybinding, KEYSYM for keyboard, MOUSE for mouse.
GLFW.GLFW_KEY_RIGHT_BRACKET, // The keycode of the key
"category.nec_test" // The translation key of the keybinding's category.
));
@Override
public void onInitializeClient() {
ClientTickEvents.END_CLIENT_TICK.register(client -> {
if (tickKeyBinding.wasPressed()) {
throw new NecTestCrash("Test Game Loop Crash");
}
if (localeKeyBinding.wasPressed()) {
throw new NecTestCrash("שלום עולם");
}
});
}
}
================================================
FILE: TestFabricMod/src/main/java/io/github/natanfudge/nectest/TestSuppressedCloseable.java
================================================
package io.github.natanfudge.nectest;
public class TestSuppressedCloseable implements AutoCloseable {
@Override
public void close() {
throw new NecTestCrash("Test Suppressed Exception");
}
}
================================================
FILE: TestFabricMod/src/main/java/io/github/natanfudge/nectest/mixin/ExampleMixin.java
================================================
package io.github.natanfudge.nectest.mixin;
import net.minecraft.client.gui.screen.TitleScreen;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(TitleScreen.class)
public class ExampleMixin {
@Inject(at = @At("HEAD"), method = "init()V")
private void init(CallbackInfo info) {
System.out.println("Test NEC crash mod mixins applied");
}
}
================================================
FILE: TestFabricMod/src/main/java/io/github/natanfudge/nectest/mixin/MixinMinecraftServer.java
================================================
package io.github.natanfudge.nectest.mixin;
import io.github.natanfudge.nectest.NecTestCrash;
import io.github.natanfudge.nectest.NecTestMod;
import net.minecraft.server.MinecraftServer;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(MinecraftServer.class)
public class MixinMinecraftServer {
private static boolean crashed = false;
@Inject(method = "runServer", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/MinecraftServer;endTickMetrics()V"))
private void testServerCrash(CallbackInfo ci) {
if (!crashed && NecTestMod.getTestMode().equals("server_crash")) {
crashed = true;
throw new NecTestCrash("Test server crash");
}
}
}
================================================
FILE: TestFabricMod/src/main/resources/fabric.mod.json
================================================
{
"schemaVersion": 1,
"id": "nec_testmod",
"version": "${version}",
"name": "Not Enough Crashes Test Mod",
"description": "Tests Not Enough Crashes",
"authors": [
"Fudge"
],
"contact": {
"sources": "https://github.com/natanfudge/Not-Enough-Crashes"
},
"license": "MIT",
"environment": "*",
"entrypoints": {
"main": [
"io.github.natanfudge.nectest.NecTestMod"
],
"client": [
"io.github.natanfudge.nectest.NecTestModClient"
]
},
"mixins": [
"nec_testmod.mixins.json"
],
"depends": {
"fabricloader": ">=0.11.3",
"java": ">=16"
},
"suggests": {
"another-mod": "*"
}
}
================================================
FILE: TestFabricMod/src/main/resources/nec_testmod.mixins.json
================================================
{
"required": true,
"minVersion": "0.8",
"package": "io.github.natanfudge.nectest.mixin",
"compatibilityLevel": "JAVA_21",
"mixins": [
"MixinMinecraftServer"
],
"client": [
"ExampleMixin"
],
"injectors": {
"defaultRequire": 1
}
}
================================================
FILE: TestForgeMod/build.gradle
================================================
plugins {
alias libs.plugins.shadow
}
archivesBaseName = "nec_testmod"
version = "1.0.0"
group = "io.github.natanfudge"
configurations {
shadowCommon // Don't use shadow from the shadow plugin because we don't want IDEA to index this.
}
loom {
forge {
mixinConfigs = ["nec_testmod.mixins.json"]
}
silentMojangMappingsLicense()
}
dependencies {
forge "net.minecraftforge:forge:${libs.versions.minecraft.get()}-${libs.versions.forge.get()}"
// Remove the next line if you don't want to depend on the API
modApi libs.architectury.api
}
processResources {
inputs.property "version", project.version
filesMatching("META-INF/mods.toml") {
expand "version": project.version
}
}
shadowJar {
exclude "fabric.mod.json"
configurations = [project.configurations.shadowCommon]
archiveClassifier.set "dev-shadow"
}
remapJar {
input.set shadowJar.archiveFile
dependsOn shadowJar
archiveClassifier.set "forge"
}
jar {
archiveClassifier.set "dev"
}
tasks.withType(JavaCompile) {
options.encoding = "UTF-8"
options.release = 16
}
================================================
FILE: TestForgeMod/gradle.properties
================================================
# Necessary for Architectury Loom to build against Forge
loom.platform=forge
================================================
FILE: TestForgeMod/src/main/java/io/github/natanfudge/nectest/NecTestCrash.java
================================================
package io.github.natanfudge.nectest;
public class NecTestCrash extends RuntimeException{
public NecTestCrash(String message){
super(message);
}
}
================================================
FILE: TestForgeMod/src/main/java/io/github/natanfudge/nectest/NecTestMod.java
================================================
package io.github.natanfudge.nectest;
import cpw.mods.modlauncher.Launcher;
import net.minecraft.block.Blocks;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.event.TickEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent;
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
import net.minecraftforge.fml.loading.FMLLoader;
import net.minecraftforge.fml.loading.FMLPaths;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
// The value here should match an entry in the META-INF/mods.toml file
@Mod("nec_testmod")
public class NecTestMod {
public static String getTestMode() {
Path configDig = FMLPaths.CONFIGDIR.get();
Path testModePath = configDig.resolve("nec_test_mode.txt");
try {
Files.createDirectories(testModePath.getParent());
if (!Files.exists(testModePath)) {
Files.createFile(testModePath);
}
return new String(Files.readAllBytes(testModePath));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
// Directly reference a log4j logger.
private static final Logger LOGGER = LogManager.getLogger();
public NecTestMod() {
if (getTestMode().equals("init_crash")) {
throw new NecTestCrash("Test Init Crash");
}
// Register the setup method for modloading
FMLJavaModLoadingContext.get().getModEventBus().addListener(this::setup);
if (FMLLoader.getDist() == Dist.CLIENT) {
FMLJavaModLoadingContext.get().getModEventBus().addListener(NecTestModClient::onKeyRegister);
}
// Register ourselves for server and other game events we are interested in
MinecraftForge.EVENT_BUS.register(this);
}
private void setup(final FMLCommonSetupEvent event) {
// some preinit code
LOGGER.info("HELLO FROM PREINIT");
LOGGER.info("DIRT BLOCK >> {}", Blocks.DIRT.getName());
}
// You can use EventBusSubscriber to automatically subscribe events on the contained class (this is subscribing to the MOD
// Event bus for receiving Registry Events)
@Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.FORGE)
public static class RegistryEvents {
//TODO: this is the wrong event subscription
@SubscribeEvent
public static void onClientTick(final TickEvent.ClientTickEvent event) {
if (NecTestModClient.key.isPressed()) {
throw new NecTestCrash("test crash");
}
}
}
}
================================================
FILE: TestForgeMod/src/main/java/io/github/natanfudge/nectest/NecTestModClient.java
================================================
package io.github.natanfudge.nectest;
import net.minecraft.client.option.KeyBinding;
import net.minecraftforge.client.event.RegisterKeyMappingsEvent;
import org.lwjgl.glfw.GLFW;
public class NecTestModClient {
public static final KeyBinding key = new KeyBinding("key.nec_test.crash", GLFW.GLFW_KEY_LEFT_BRACKET, "category.nec_test");
public static void onKeyRegister(RegisterKeyMappingsEvent event) {
event.register(key);
}
}
================================================
FILE: TestForgeMod/src/main/java/io/github/natanfudge/nectest/mixin/ExampleMixin.java
================================================
package io.github.natanfudge.nectest.mixin;
import net.minecraft.client.gui.screen.TitleScreen;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(TitleScreen.class)
public class ExampleMixin {
@Inject(at = @At("HEAD"), method = "init()V")
private void init(CallbackInfo info) {
System.out.println("Test NEC crash mod mixins applied");
}
}
================================================
FILE: TestForgeMod/src/main/java/io/github/natanfudge/nectest/mixin/MixinMinecraftServer.java
================================================
package io.github.natanfudge.nectest.mixin;
import io.github.natanfudge.nectest.NecTestCrash;
import io.github.natanfudge.nectest.NecTestMod;
import net.minecraft.server.MinecraftServer;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(MinecraftServer.class)
public class MixinMinecraftServer {
private static boolean crashed = false;
@Inject(method = "runServer", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/MinecraftServer;endTickMetrics()V"))
private void testServerCrash(CallbackInfo ci) {
if (!crashed && NecTestMod.getTestMode().equals("server_crash")) {
crashed = true;
throw new NecTestCrash("Test server crash");
}
}
}
================================================
FILE: TestForgeMod/src/main/resources/META-INF/mods.toml
================================================
# This is an example mods.toml file. It contains the data relating to the loading mods.
# There are several mandatory fields (#mandatory), and many more that are optional (#optional).
# The overall format is standard TOML format, v0.5.0.
# Note that there are a couple of TOML lists in this file.
# Find more information on toml format here: https://github.com/toml-lang/toml
# The name of the mod loader type to load - for regular FML @Mod mods it should be javafml
modLoader="javafml" #mandatory
# A version range to match for said mod loader - for regular FML @Mod it will be the forge version
loaderVersion="[37,)" #mandatory This is typically bumped every Minecraft version by Forge. See our download page for lists of versions.
# The license for you mod. This is mandatory metadata and allows for easier comprehension of your redistributive properties.
# Review your options at https://choosealicense.com/. All rights reserved is the default copyright stance, and is thus the default here.
license="All rights reserved"
# A URL to refer people to when problems occur with this mod
#issueTrackerURL="https://change.me.to.your.issue.tracker.example.invalid/" #optional
# A list of mods - how many allowed here is determined by the individual mod loader
[[mods]] #mandatory
# The modid of the mod
modId="nec_testmod" #mandatory
# The version number of the mod - there's a few well known ${} variables useable here or just hardcode it
# see the associated build.gradle script for how to populate this completely automatically during a build
version = "${version}"
# A display name for the mod
displayName="Not Enough Crashes Test Mod" #mandatory
# A URL to query for updates for this mod. See the JSON update specification https://mcforge.readthedocs.io/en/latest/gettingstarted/autoupdate/
#updateJSONURL="https://change.me.example.invalid/updates.json" #optional
# A URL for the "homepage" for this mod, displayed in the mod UI
#displayURL="https://change.me.to.your.mods.homepage.example.invalid/" #optional
# A file name (in the root of the mod JAR) containing a logo for display
logoFile="examplemod.png" #optional
# A text field displayed in the mod UI
credits="Thanks for this example mod goes to Java" #optional
# A text field displayed in the mod UI
authors="Love, Cheese and small house plants" #optional
# The description text for the mod (multi line!) (#mandatory)
description='''
This is a long form description of the mod. You can write whatever you want here
Have some lorem ipsum.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed mollis lacinia magna. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Sed sagittis luctus odio eu tempus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Pellentesque volutpat ligula eget lacus auctor sagittis. In hac habitasse platea dictumst. Nunc gravida elit vitae sem vehicula efficitur. Donec mattis ipsum et arcu lobortis, eleifend sagittis sem rutrum. Cras pharetra quam eget posuere fermentum. Sed id tincidunt justo. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
'''
# A dependency - use the . to indicate dependency for a specific modid. Dependencies are optional.
[[dependencies.nec_testmod]] #optional
# the modid of the dependency
modId="forge" #mandatory
# Does this dependency have to exist - if not, ordering below must be specified
mandatory=true #mandatory
# The version range of the dependency
versionRange="[37,)" #mandatory
# An ordering relationship for the dependency - BEFORE or AFTER required if the relationship is not mandatory
ordering="NONE"
# Side this dependency is applied on - BOTH, CLIENT or SERVER
side="BOTH"
# Here's another dependency
[[dependencies.examplemod]]
modId="minecraft"
mandatory=true
# This version range declares a minimum of the current minecraft version up to but not including the next major version
versionRange="[1.17.1,1.18)"
ordering="NONE"
side="BOTH"
================================================
FILE: TestForgeMod/src/main/resources/nec_testmod.mixins.json
================================================
{
"required": true,
"minVersion": "0.8",
"package": "io.github.natanfudge.nectest.mixin",
"compatibilityLevel": "JAVA_17",
"mixins": [
"MixinMinecraftServer"
],
"client": [
"ExampleMixin"
],
"injectors": {
"defaultRequire": 1
}
}
================================================
FILE: TestForgeMod/src/main/resources/pack.mcmeta
================================================
{
"pack": {
"description": "examplemod resources",
"pack_format": 7
}
}
================================================
FILE: TooManyCrashes.license.md
================================================
MIT License
Copyright (c) 2018 Dimensional Development
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: build.gradle
================================================
plugins {
alias libs.plugins.architectury.loom apply false
alias libs.plugins.minotaur apply false
alias libs.plugins.architectury.plugin
alias libs.plugins.cursegradle
id 'maven-publish'
}
architectury {
minecraft = libs.versions.minecraft.get()
}
subprojects {
apply plugin: 'dev.architectury.loom'
apply plugin: 'architectury-plugin'
apply plugin: 'maven-publish'
apply plugin: "com.modrinth.minotaur"
apply plugin: 'com.matthewprenger.cursegradle'
def modVersion = libs.versions.mod.version.get()
def minecraftVersion = libs.versions.minecraft.get()
def total_version = "$modVersion+$minecraftVersion"
ext.total_version = total_version
def cf = libs.versions.overrides.curseforge.get()
def mr = libs.versions.overrides.modrinth.get()
ext.curseforge_mc_version = cf == 'mc' ? minecraftVersion : cf
ext.modrinth_mc_version = mr == 'mc' ? minecraftVersion : mr
ext.release_type = libs.versions.release.type.get()
archivesBaseName = rootProject.archives_base_name
version = ext.total_version
group = rootProject.maven_group
base {
// Set up a suffixed format for the mod jar names, e.g. `example-fabric`.
archivesName = "$rootProject.archives_base_name-$project.name"
}
dependencies {
minecraft libs.minecraft
mappings loom.layered {
it.mappings("net.fabricmc:yarn:${libs.versions.yarn.mappings.version.get()}:v2")
it.mappings libs.yarn.mappings.patch.neoforge
// it.officialMojangMappings()
}
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(21))
}
withSourcesJar()
}
}
task publishAll {
group = "upload"
dependsOn("fabric:publishFabric")
dependsOn("neoforge:publishForge")
}
//task addTestFabricMod(type: Copy) {
// dependsOn project(":TestFabricMod").tasks.build
// from "TestFabricMod/build/libs/nec_testmod-1.0.0.jar"
// into "fabric/run/mods"
//}
//
//task addTestForgeMod(type: Copy) {
// dependsOn project(":TestForgeMod").tasks.build
//// Copy dev jar because there is no support for automatic remapping
// from "TestForgeMod/build/libs/nec_testmod-1.0.0-dev-shadow.jar"
// into "forge/run/mods"
//}
//
//task addTestMods {
// dependsOn addTestFabricMod, addTestForgeMod
//}
================================================
FILE: changelog.md
================================================
### 4.4.9
- Added Japanese Translation
### 4.4.8
- Removed a fringe feature that was eating away at the computer's memory
# Minecraft 1.20.6
- Starting from Minecraft 1.20.6, NEC supports **only** NeoForge, and not Forge.
### 4.4.7
- Fixed crash in certain cases
### 4.4.6
- Fixed quit button not have the correct text in some cases.
- Crash initialization screen is now disabled when Sodium is installed.
### 4.4.5
- Fixed a crash in Fabric with certain mods.
### 4.4.4
- Cleaned up some more things when the game crashes.
### 4.4.3
- Updated Chinese translations
### 4.4.1
- Fixed gibrish text sometimes showing in the crash screen.
- Added Ukrainian translations (thanks @PetroTornados!).
- Fixed incorrect tab ordering in the config screen.
## 4.4.0
- Brand new GUI configuration screen for Not Enough Crashes. Accessible through ModMenu in Fabric and through the regular Forge mod menu.
- crashlogUpload, deobfuscateStackTrace and forceCrashScreen config options are deprecated and will no longer work.
## 4.3.0
- Updated internals to better support Crashy 1.0 (older versions of NEC will still work with Crashy)
- Will no longer deobfuscate stack traces in any way. This feature is now supported exceptionally well by [Crashy 1.0](https://crashy.net/).
### 4.2.0
- Now identifies mods that applied mixins to a crash stack trace, which means that more mods will be identified as a potential cause for a crash. (Thanks sschr15!)
### 4.1.8
- Added and fixed Chinese translations (thanks SolidBlock-cn!).
### 4.1.7
- Fixed the mod claiming it must be installed on the server on Forge.
### 4.1.6
- Updated to support Quilt hashed mappings.
### 4.1.5
- Fixed not working on Quilt.
### 4.1.4
- Fixed the text of the crash screen sometimes being broken
- Fixed not being able to use custom assets for the mod. Note: this will still not work in Fabric if Fabric API is not installed.
### 4.1.3
- Fixed mixin errors being printed to the log on startup.
### 4.1.2
- Fixed the crash screen not working in Forge.
### 4.1.1
- Fixed the crash report being printed to the log twice on integrated server crashes.
- Fixed the crash report txt file missing information on integrated server crashes.
- Fixed Not Enough Crashes being blamed for any crash after the first one in a single game session.
- Fixed mods being blamed incorrectly when the minecraft instance path contains spaces.
- Fixed integrated server crashes not being caught in Forge.
# 4.0.0
- **All version from 4.0.0 onwards only support Fabric Loader 0.12.0 and above**.
### 3.7.2
All version from 2.0.0 to 3.7.2 only support Fabric Loader versions from 0.9.0 to 0.11.7.
This will now be validated by Fabric Loader.
### 3.7.1
- Fixed ['Exiting world while F3+L profiling is active crashes recursively.'](https://github.com/natanfudge/Not-Enough-Crashes/issues/83).
## 3.7.0
- Introducing: [Crashy](https://crashy.net/)! Crashy is a crash hosting site designed specifically for Not Enough Crashes and Minecraft crashes in general.
It shows crashes in an organized GUI that is easily readable, and has some other nice features. [Example](https://crashy.net/2c2vAe5oUVgiNck3NfXU).
There is now a button for uploading directly to crashy in the crash screen.
- Fixed UTF-specific characters turning into '?' when uploading crash logs.
- Fixed 'Continuing the game after crashing will cause a crash report to be logged later when the game exits normally'.
### 3.6.5
- Forge for 1.17.1! As this is the first Forge version in a while it may have some issues, so comment if you've encountered anything (or better, open a Github issue).
### 3.6.4
- Added some Quilt-specific features, courtesy of @Siuolthepic!
### 3.6.3
- Quilt now absolutely, officially, works, just as well as Fabric does.
### 3.6.2
- Fixed text not being localized to English when the translation is not available for the chosen language.
### 3.6.1
- Should now work with the Quilt mod loader, with `deobfuscateStackTrace` set to false in the config.
Currently, catching initialization errors and deobfuscation is not supported in Quilt.
## 3.6.0
- ~~Should now work with the Quilt mod loader~~ not yet
- Fixed Jar-in-jar mods not being blamed for crashes
## 3.5.0
- No longer depends on Fabric API for localization to work properly.
### 3.4.5
- Fixed some regressions in cleaning up after crash. This fixes not being disconnected from servers.
### 3.4.4
- Fixed integrated server crashes not being caught.
- Fixed state sometimes not being cleaned up properly which could cause the game to infinitely crash.
### 3.4.3
- Fixed deobfuscation not working in dedicated servers, courtesy of @Fourmisain!
### 3.4.2
- Prevented extreme cases where the crash log could become incredibly large.
- Fixed the crash screen not showing suspected mods in cases where adding them to the crash log was prevented by conflicting mods.
### 3.4.1
- Improved internal error message.
## 3.4.0
- Provided many configuration options for uploading the crash logs, see NecConfig.java, thanks to The456Gamer!
- The config format for uploading crash logs has changed, refer to NecConfig.java for the new format.
## 3.3.1
- Fixed mod identification not working.
- Fixed deobfuscation sometimes not working.
- Promoted to Release!
## 3.3.0
- Updated to Minecraft 1.17, Java 16.
- Currently buggy, this is a minimum viable alpha release.
## 3.2.0
- Added a new option `forceCrashScreen` that will prevent cases in which the game closes with no crash log. Instead, the game will crash normally.
### 3.1.9
- Fixed additional crash stack traces appearing when debugModIdentification is false.
### 3.1.8
- The 1.16.5 Fabric version will no longer deliberately crash in 1.17 snapshots. This means that it may work if nothing broke the mod in a snapshot.
### 3.1.7
- Added a new config option: debugModIdentification, that will hopefully help in discovering mods in more cases.
### 3.1.6
- (Forge) Fixed being unable to identify crashing mods that have multiple authors.
### 3.1.5
- Made it so the Curseforge page can be reached through the Mod Menu entry.
### 3.1.4
- Fixed Forge version not working at all.
### 3.1.3
- Made NEC more compatible with other mods, specifically with [Structure Gel API](https://www.curseforge.com/minecraft/mc-mods/structure-gel-api).
### 3.1.1
- Fixed mod not loading.
## 3.1.0
- Added extra info for feature/structure crashes to make it much easier to find the problematic mod, courtesy of TelepathicGrunt!.
# 3.0.0
- Now supports Forge!
## 2.2.0
- Removed 'feature' that would instantly crash the game when pressing F3+C instead of after 6 seconds, since that hotkey is used to copy location information.
- Added a proper mod icon.
### 2.1.4
- Improved Simplified Chinese localization, courtesy of @WuzgXY!
### 2.1.3
- Fixed "upload crashlog" not working :) .
### 2.1.1
- Fixed "upload crashlog" not working.
## 2.1.0
- Added Estonian localization, courtesy of @Madis0!
# 2.0.0
- Fixed the mod not working in Fabric Loader 0.9.0+, however, this and the following versions will only work for the 0.9.0+ Fabric Loader versions.
### 1.2.4
- Will no longer blame jumploader for errors all the time
### 1.2.3
- Fixed an incompatibility with LambdaControls (mostly just a mistake, not really an incompatibility).
- Crash logs will now be uploaded as gists instead of to the dimdev haste. This can be reverted by setting `uploadCrashLogTo` to `DIMDEV_HASTE` in the config.
- Fixed some GUI bugs in the crash scree
### 1.2.1
- Compatibility with [Multiconnect](https://www.curseforge.com/minecraft/mc-mods/multiconnect/files).
Note: Informed Load has yet to publish the compatible version.
## 1.2.0
- Compatibility with [Informed Load](https://www.curseforge.com/minecraft/mc-mods/informed-load).
### 1.1.5
- Fixed a rare error.
### 1.1.4
- Fixed the window not closing when pre-initialization errors occur.
### 1.1.3
- Initialization errors will now be printed as soon as an error is caught, instead of only when displaying the crash screen.
- The init error screen will now display in more disaster cases.
### 1.1.2
- Fixed the entry point catcher not being enabled...
### 1.1.1
- Forgot a debug flag
## 1.1.0
- Added an API for running code when the game crashes to prevent the window getting stuck in weird states.
### 1.0.11
- Fixed two "Not Enough Crashes deobfuscated stack trace" lines appearing (instead of just one).
### 1.0.10
- Fixed an exception while starting game.
# 1.0.9
Released
================================================
FILE: common/build.gradle
================================================
architectury {
common("fabric","neoforge")
}
dependencies {
// We depend on Fabric Loader here to use the Fabric @Environment annotations,
// which get remapped to the correct annotations on each platform.
// Do NOT use other classes from Fabric Loader.
modImplementation libs.fabric.loader
}
================================================
FILE: common/src/main/java/fudge/notenoughcrashes/NotEnoughCrashes.java
================================================
package fudge.notenoughcrashes;
import fudge.notenoughcrashes.config.MidnightConfig;
import fudge.notenoughcrashes.config.NecConfig;
import fudge.notenoughcrashes.config.NecMidnightConfig;
import fudge.notenoughcrashes.platform.CommonModMetadata;
import fudge.notenoughcrashes.platform.NecPlatform;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
public class NotEnoughCrashes {
public static final Path DIRECTORY = NecPlatform.instance().getGameDirectory().resolve("not-enough-crashes");
public static final String NAME = "Not Enough Crashes";
public static final String MOD_ID = "notenoughcrashes";
public static Logger getLogger() {
// Create a new logger every time because Forge blocks loggers created in one context from working in another context.
return LogManager.getLogger(NAME);
}
private static final boolean LOG_DEBUG = false;
public static void logDebug(String message) {
if (LOG_DEBUG) getLogger().error(message);
}
public static boolean enableGameloopCatching() {
return NecConfig.getCurrent().catchGameloopCrashes() && !NecPlatform.instance().irisExists();
}
public static boolean enableEntrypointCatching() {
return NecConfig.getCurrent().catchInitializationCrashes() && !NecPlatform.instance().irisExists()
// Sodium insists on crashing when windows are used before it is initialized
&& !NecPlatform.instance().isModLoaded("sodium");
}
public static CommonModMetadata getMetadata() {
List<CommonModMetadata> mods = NecPlatform.instance().getModMetadatas(MOD_ID);
if (mods.size() != 1) throw new IllegalStateException("NEC should have exactly one mod under its ID");
return mods.get(0);
}
public static void ensureDirectoryExists() throws IOException {
Files.createDirectories(DIRECTORY);
}
public static void initialize() {
// if (NecConfig.getCurrent().forceCrashScreen()) SystemExitBlock.forbidSystemExitCall();
MidnightConfig.init(NotEnoughCrashes.MOD_ID, NecMidnightConfig.class);
}
}
================================================
FILE: common/src/main/java/fudge/notenoughcrashes/StateManager.java
================================================
package fudge.notenoughcrashes;
import java.lang.ref.WeakReference;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
///**
// * Allows registering objects to be reset after a crash. Objects registered
// * use WeakReferences, so they will be garbage-collected despite still being
// * registered here.
// */
//public class StateManager {
//
// // Use WeakReference to allow garbage collection, preventing memory leaks
// private static final Set<WeakReference<IResettable>> resettableRefs = new HashSet<>();
//
// public static void resetStates() {
// Iterator<WeakReference<IResettable>> iterator = resettableRefs.iterator();
// while (iterator.hasNext()) {
// IResettable ref = iterator.next().get();
// if (ref != null) {
// ref.resetState();
// } else {
// iterator.remove();
// }
// }
// }
//
// public interface IResettable {
// default void register() {
// resettableRefs.add(new WeakReference<>(this));
// }
//
// void resetState();
// }
//}
================================================
FILE: common/src/main/java/fudge/notenoughcrashes/api/NotEnoughCrashesApi.java
================================================
package fudge.notenoughcrashes.api;//package fudge.notenoughcrashes.api;
//
//import java.util.ArrayList;
//import java.util.List;
//
//public class NotEnoughCrashesApi {
// public static final List<Runnable> permanentDisposers = new ArrayList<>();
// public static final List<Runnable> oneTimeDisposers = new ArrayList<>();
//
// public static void onEveryCrash(Runnable disposer){
// permanentDisposers.add(disposer);
// }
//
// public static void onNextCrash(Runnable disposer) {
// oneTimeDisposers.add(disposer);
// }
//}
//
================================================
FILE: common/src/main/java/fudge/notenoughcrashes/config/ButtonEntry.java
================================================
package fudge.notenoughcrashes.config;
import com.google.common.collect.Lists;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.font.TextRenderer;
import net.minecraft.client.gui.Click;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.Element;
import net.minecraft.client.gui.Selectable;
import net.minecraft.client.gui.screen.ConfirmLinkScreen;
import net.minecraft.client.gui.widget.ClickableWidget;
import net.minecraft.client.gui.widget.ElementListWidget;
import net.minecraft.client.gui.widget.MultilineTextWidget;
import net.minecraft.registry.Registries;
import net.minecraft.text.Text;
import net.minecraft.util.Identifier;
import java.util.List;
public class ButtonEntry extends ElementListWidget.Entry<ButtonEntry> {
private static final TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer;
public final Text text;
public final List<ClickableWidget> buttons;
public final EntryInfo info;
public boolean centered = false;
public MultilineTextWidget title;
public ButtonEntry(List<ClickableWidget> buttons, Text text, EntryInfo info) {
this.buttons = buttons;
this.text = text;
this.info = info;
if (info != null && info.comment != null)
this.centered = info.comment.centered();
int scaledWidth = MinecraftClient.getInstance().getWindow().getScaledWidth();
if (text != null && (!text.getString().contains("spacer") || !buttons.isEmpty())) {
title = new MultilineTextWidget(12, 0, Text.of(text), textRenderer).setCentered(centered);
if (info != null)
title.setTooltip(info.getTooltip(false));
title.setMaxWidth(!buttons.isEmpty() ? buttons.get(buttons.size() > 2 ? buttons.size() - 1 : 0).getX() - 16 : scaledWidth - 24);
if (centered) title.setX(scaledWidth / 2 - (title.getWidth() / 2));
}
}
public void render(DrawContext context, int mouseX, int mouseY, boolean hovered, float tickDelta) {
buttons.forEach(b -> {
b.setY(this.getY());
b.render(context, mouseX, mouseY, tickDelta);
});
if (title != null) {
title.setY(this.getY() + 5);
title.render(context, mouseX, mouseY, tickDelta);
if (info.entry != null && !this.buttons.isEmpty() && this.buttons.getFirst() instanceof ClickableWidget widget) {
int idMode = this.info.entry.idMode();
if (idMode != -1) context.drawItem(idMode == 0 ?
Registries.ITEM.get(Identifier.tryParse(this.info.tempValue)).getDefaultStack()
: Registries.BLOCK.get(Identifier.tryParse(this.info.tempValue)).asItem().getDefaultStack(),
widget.getX() + widget.getWidth() - 18, this.getY() + 2);
}
}
}
@Override
public boolean mouseClicked(Click click, boolean doubled) {
if (this.info != null && this.info.comment != null && !this.info.comment.url().isBlank())
ConfirmLinkScreen.open(MinecraftClient.getInstance().currentScreen, this.info.comment.url(), true);
return super.mouseClicked(click, doubled);
}
public List<? extends Element> children() {
return Lists.newArrayList(buttons);
}
public List<? extends Selectable> selectableChildren() {
return Lists.newArrayList(buttons);
}
}
================================================
FILE: common/src/main/java/fudge/notenoughcrashes/config/EntryInfo.java
================================================
package fudge.notenoughcrashes.config;
import fudge.notenoughcrashes.platform.NecPlatform;
import net.minecraft.client.gui.tab.Tab;
import net.minecraft.client.gui.tooltip.Tooltip;
import net.minecraft.client.gui.widget.ClickableWidget;
import net.minecraft.client.resource.language.I18n;
import net.minecraft.text.Text;
import java.lang.reflect.Field;
import java.util.List;
public class EntryInfo {
public MidnightConfig.Entry entry;
public MidnightConfig.Comment comment;
public MidnightConfig.Condition[] conditions;
public final Field field;
public final Class<?> dataType;
public final String modid, fieldName, translationKey;
int listIndex;
Object defaultValue, value, function;
String tempValue; // The value visible in the config screen
boolean inLimits = true;
Text error;
ClickableWidget actionButton; // color picker button / explorer button
Tab tab;
boolean conditionsMet = true;
public EntryInfo(Field field, String modid) {
this.field = field;
this.modid = modid;
if (field != null) {
this.fieldName = field.getName();
this.dataType = MidnightConfig.getUnderlyingType(field);
this.entry = field.getAnnotation(MidnightConfig.Entry.class);
this.comment = field.getAnnotation(MidnightConfig.Comment.class);
this.conditions = field.getAnnotationsByType(MidnightConfig.Condition.class);
} else {
this.fieldName = "";
this.dataType = null;
}
if (entry != null && !entry.name().isEmpty())
this.translationKey = entry.name();
else if (comment != null && !comment.name().isEmpty())
this.translationKey = comment.name();
else this.translationKey = modid + ".midnightconfig." + fieldName;
}
public void setValue(Object value) {
if (this.field.getType() != List.class) {
this.value = value;
this.tempValue = value.toString();
} else {
writeList(this.listIndex, value);
this.tempValue = toTemporaryValue();
}
}
public String toTemporaryValue() {
if (this.field.getType() != List.class) return this.value.toString();
else try {
return ((List<?>) this.value).get(this.listIndex).toString();
} catch (Exception ignored) {
return "";
}
}
public void updateFieldValue() {
try {
if (this.field.get(null) != value) MidnightConfig.entries.values().forEach(EntryInfo::updateConditions);
this.field.set(null, this.value);
} catch (IllegalAccessException ignored) {
}
}
public void updateConditions() {
boolean prevConditionState = this.conditionsMet;
if (this.conditions.length > 0) this.conditionsMet = true; // reset conditions
for (MidnightConfig.Condition condition : this.conditions) {
//noinspection ConstantValue
if (!condition.requiredModId().isEmpty() && !NecPlatform.instance().isModLoaded(condition.requiredModId()))
this.conditionsMet = false;
String requiredOption = condition.requiredOption().contains(":") ? condition.requiredOption() : (this.modid + ":" + condition.requiredOption());
if (MidnightConfig.entries.get(requiredOption) instanceof EntryInfo info)
this.conditionsMet &= List.of(condition.requiredValue()).contains(info.tempValue);
if (!this.conditionsMet) break;
}
if (prevConditionState != this.conditionsMet) MidnightConfig.configInstances.get(modid).reloadScreen = true;
}
public <T> void writeList(int index, T value) {
//noinspection unchecked
var list = (List<T>) this.value;
if (index >= list.size())
list.add(value);
else list.set(index, value);
}
public Tooltip getTooltip(boolean isButton) {
String key = translationKey + (!isButton ? ".label" : "") + ".tooltip";
return Tooltip.of(isButton && this.error != null ? this.error : I18n.hasTranslation(key) ? Text.translatable(key) : Text.empty());
}
}
================================================
FILE: common/src/main/java/fudge/notenoughcrashes/config/MidnightConfig.java
================================================
package fudge.notenoughcrashes.config;
import com.google.gson.*;
import com.google.gson.stream.*;
import fudge.notenoughcrashes.platform.NecPlatform;
import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.gui.widget.*;
import net.minecraft.client.resource.language.I18n;
import net.minecraft.text.Style;
import net.minecraft.text.Text;
import net.minecraft.util.*;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.Color;
import java.io.IOException;
import java.lang.annotation.*;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.regex.Pattern;
/** MidnightConfig by Martin "Motschen" Prokoph
* Minimalist config library - feel free to copy!
* Originally based on <a href="https://github.com/Minenash/TinyConfig">...</a>
* Credits to Minenash */
@SuppressWarnings("unchecked")
public abstract class MidnightConfig {
private static final Pattern INTEGER_ONLY = Pattern.compile("(-?[0-9]*)");
private static final Pattern DECIMAL_ONLY = Pattern.compile("-?(\\d+\\.?\\d*|\\d*\\.?\\d+|\\.)");
private static final Pattern HEXADECIMAL_ONLY = Pattern.compile("(-?[#0-9a-fA-F]*)");
private static final Gson gson = new GsonBuilder()
.excludeFieldsWithModifiers(Modifier.TRANSIENT).excludeFieldsWithModifiers(Modifier.PRIVATE).excludeFieldsWithModifiers(Modifier.FINAL)
.addSerializationExclusionStrategy(new ExclusionStrategy() {
public boolean shouldSkipClass(Class<?> clazz) { return false; }
public boolean shouldSkipField(FieldAttributes fieldAttributes) { return fieldAttributes.getAnnotation(Entry.class) == null; }
})
.registerTypeAdapter(Identifier.class, new TypeAdapter<Identifier>() {
public void write(JsonWriter out, Identifier id) throws IOException { out.value(id.toString()); }
public Identifier read(JsonReader in) throws IOException { return Identifier.of(in.nextString()); }
}).setPrettyPrinting().create();
protected static final LinkedHashMap<String, EntryInfo> entries = new LinkedHashMap<>(); // modid:fieldName -> EntryInfo
public static final Map<String, MidnightConfig> configInstances = new HashMap<>();
protected String modid;
protected boolean reloadScreen = false;
public Class<? extends MidnightConfig> configClass;
public static <T extends MidnightConfig> T createInstance(String modid, Class<? extends MidnightConfig> configClass) { // This is basically an argumented constructor without the requirement of having one in each config class
try {
T instance = (T) configClass.getDeclaredConstructor().newInstance();
instance.modid = modid;
instance.configClass = configClass;
configInstances.put(modid, instance);
return instance;
}
catch (Exception e) { throw new RuntimeException(e); }
}
public static void init(String modid, Class<? extends MidnightConfig> config) {
MidnightConfig instance = createInstance(modid, config);
for (Field field : config.getFields()) {
//noinspection ConstantValue
if ((field.isAnnotationPresent(Entry.class) || field.isAnnotationPresent(Comment.class))
&& !field.isAnnotationPresent(Server.class)
&& !field.isAnnotationPresent(Hidden.class)
&& NecPlatform.instance().isClientEnv())
instance.addClientEntry(field, new EntryInfo(field, modid));
}
instance.loadValuesFromJson();
}
public void addClientEntry(Field field, EntryInfo info) {
Entry e = info.entry;
if (e != null && info.dataType != null) {
if (info.dataType == int.class) textField(info, Integer::parseInt, INTEGER_ONLY, (int) e.min(), (int) e.max(), true);
else if (info.dataType == float.class) textField(info, Float::parseFloat, DECIMAL_ONLY, (float) e.min(), (float) e.max(), false);
else if (info.dataType == double.class) textField(info, Double::parseDouble, DECIMAL_ONLY, e.min(), e.max(), false);
else if (info.dataType == String.class || info.dataType == Identifier.class) textField(info, String::length, null, Math.min(e.min(), 0), Math.max(e.max(), 1), true);
else if (info.dataType == boolean.class) {
Function<Object, Text> func = value -> Text.translatable((Boolean) value ? "gui.yes" : "gui.no").formatted((Boolean) value ? Formatting.GREEN : Formatting.RED);
info.function = new AbstractMap.SimpleEntry<ButtonWidget.PressAction, Function<Object, Text>>(button -> {
info.setValue(!(Boolean) info.value); button.setMessage(func.apply(info.value));
}, func);
} else if (info.dataType.isEnum()) {
List<?> values = Arrays.asList(field.getType().getEnumConstants());
Function<Object, Text> func = value -> getEnumTranslatableText(value, info);
info.function = new AbstractMap.SimpleEntry<ButtonWidget.PressAction, Function<Object, Text>>(button -> {
int index = values.indexOf(info.value) + 1;
info.setValue(values.get(index >= values.size() ? 0 : index));
button.setMessage(func.apply(info.value));
}, func);
}
try { info.defaultValue = field.get(null);
} catch (IllegalAccessException ignored) {}
}
entries.put(modid + ":" + field.getName(), info);
}
public static Class<?> getUnderlyingType(Field field) {
Class<?> rawType = field.getType();
if (field.getType() == List.class)
rawType = (Class<?>) ((ParameterizedType) field.getGenericType()).getActualTypeArguments()[0];
try { return (Class<?>) rawType.getField("TYPE").get(null); // Tries to get primitive types from non-primitives (e.g. Boolean -> boolean)
} catch (NoSuchFieldException | IllegalAccessException ignored) { return rawType; }
}
private static void textField(EntryInfo info, Function<String,Number> f, Pattern pattern, double min, double max, boolean cast) {
boolean isNumber = pattern != null;
info.function = (BiFunction<TextFieldWidget, ButtonWidget, Predicate<String>>) (t, b) -> s -> {
s = s.trim();
if (!(s.isEmpty() || !isNumber || pattern.matcher(s).matches()) ||
(info.dataType == Identifier.class && Identifier.validate(s).isError())) return false;
Number value = 0; boolean inLimits = false; info.error = null;
if (!(isNumber && s.isEmpty()) && !s.equals("-") && !s.equals(".")) {
try { value = f.apply(s); } catch(NumberFormatException e){ return false; }
inLimits = value.doubleValue() >= min && value.doubleValue() <= max;
info.error = inLimits? null : Text.literal(value.doubleValue() < min ?
"§cMinimum " + (isNumber? "value" : "length") + (cast? " is " + (int)min : " is " + min) :
"§cMaximum " + (isNumber? "value" : "length") + (cast? " is " + (int)max : " is " + max)).formatted(Formatting.RED);
t.setTooltip(info.getTooltip(true));
}
info.tempValue = s;
t.setEditableColor(inLimits? 0xFFFFFFFF : 0xFFFF7777);
info.inLimits = inLimits;
b.active = entries.values().stream().allMatch(e -> e.inLimits);
if (inLimits) {
if (info.dataType == Identifier.class)
info.setValue(Identifier.tryParse(s));
else info.setValue(isNumber ? value : s);
}
if (info.entry.isColor()) {
if (!s.contains("#")) s = '#' + s;
if (!HEXADECIMAL_ONLY.matcher(s).matches()) return false;
try { info.actionButton.setMessage(Text.literal("⬛").setStyle(Style.EMPTY.withColor(Color.decode(info.tempValue).getRGB())));
} catch (Exception ignored) {}
}
return true;
};
}
protected Text getEnumTranslatableText(Object value, EntryInfo info) {
if (value instanceof TranslatableOption translatableOption) return translatableOption.getText();
String translationKey = "%s.midnightconfig.enum.%s.%s".formatted(modid, info.dataType.getSimpleName(), info.toTemporaryValue());
return I18n.hasTranslation(translationKey) ? Text.translatable(translationKey) : Text.literal(info.toTemporaryValue());
}
public void loadValuesFromJson() {
try {
gson.fromJson(Files.newBufferedReader(getJsonFilePath()), configClass);
} catch (Exception e) {
write(modid);
}
entries.values().forEach(info -> {
if (info.field != null && info.entry != null) {
try {
info.value = info.field.get(null) == null ? info.defaultValue : info.field.get(null);
info.tempValue = info.toTemporaryValue();
info.updateConditions();
} catch (IllegalAccessException ignored) {}
}
});
}
public static void write(String modid) {
configInstances.get(modid).writeChanges(modid);
}
@Deprecated
public void writeChanges(String modid) {
this.writeChanges();
}
public void writeChanges() {
try {
Path path;
if (!Files.exists(path = getJsonFilePath()))
Files.createFile(path);
Files.write(path, gson.toJson(this).getBytes());
} catch (Exception e) { e.fillInStackTrace(); }
}
public Path getJsonFilePath() {
return NecPlatform.instance().getConfigDirectory().resolve(modid + ".json");
}
@SuppressWarnings("unused") // Utility for mod authors
public static @Nullable Object getDefaultValue(String modid, String entry) {
String key = modid + ":" + entry;
return entries.containsKey(key) ? entries.get(key).defaultValue : null;
}
// Overridable method
public void onTabInit(String tabName, MidnightConfigListWidget list, MidnightConfigScreen screen) {
}
public static MidnightConfigScreen getScreen(Screen parent, String modid) {
return configInstances.get(modid).getScreen(parent);
}
public MidnightConfigScreen getScreen(Screen parent) {
return new MidnightConfigScreen(parent, modid);
}
/**
* Entry Annotation<br>
* - <b>width</b>: The maximum character length of the {@link String}, {@link Identifier} or String/Identifier {@link List<>} field<br>
* - <b>min</b>: The minimum value of the <code>int</code>, <code>float</code> or <code>double</code> field<br>
* - <b>max</b>: The maximum value of the <code>int</code>, <code>float</code> or <code>double</code> field<br>
* - <b>name</b>: Will be used instead of the default translation key, if not empty<br>
* - <b>selectionMode</b>: The selection mode of the file picker button for {@link String} fields,
* -1 for none, {@link JFileChooser#FILES_ONLY} for files, {@link JFileChooser#DIRECTORIES_ONLY} for directories,
* {@link JFileChooser#FILES_AND_DIRECTORIES} for both (default: -1). Remember to set the translation key
* <code>[modid].midnightconfig.[fieldName].fileChooser.title</code> for the file picker dialog title<br>
* - <b>fileChooserType</b>: The type of the file picker button for {@link String} fields,
* can be {@link JFileChooser#OPEN_DIALOG} or {@link JFileChooser#SAVE_DIALOG} (default: {@link JFileChooser#OPEN_DIALOG}).
* Remember to set the translation key <code>[modid].midnightconfig.[fieldName].fileFilter.description</code> for the file filter description
* if <code>"*"</code> is not used as file extension<br>
* - <b>fileExtensions</b>: The file extensions for the file picker button for {@link String} fields (default: <code>{"*"}</code>),
* only works if selectionMode is {@link JFileChooser#FILES_ONLY} or {@link JFileChooser#FILES_AND_DIRECTORIES}<br>
* - <b>isColor</b>: If the field is a hexadecimal color code (default: false)<br>
* - <b>isSlider</b>: If the field is a slider (default: false)<br>
* - <b>precision</b>: The precision of the <code>float</code> or <code>double</code> field (default: 100)<br>
* - <b>category</b>: The category of the field in the config screen (default: "default")<br>
* */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Entry {
int width() default 400;
double min() default Double.MIN_NORMAL;
double max() default Double.MAX_VALUE;
String name() default "";
int selectionMode() default -1; // -1 for none, 0 for file, 1 for directory, 2 for both
int fileChooserType() default JFileChooser.OPEN_DIALOG;
String[] fileExtensions() default {"*"};
int idMode() default -1; // -1 for none, 0 for item, 1 for block
boolean isColor() default false;
boolean isSlider() default false;
int precision() default 100;
String category() default "default";
@Deprecated String requiredMod() default "";
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Client {}
/**
* Hides the entry in config screens, but still makes it accessible through the command {@code /midnightconfig MOD_ID ENTRY} and directly editing the config file.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Server {}
/**
* Hides the entry entirely.
* Accessible only through directly editing the config file.
* Perfect for saving persistent internal data.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Hidden {}
/**
* Comment Annotation<br>
* - <b>{@link Comment#centered()}</b>: If the comment should be centered<br>
* - <b>{@link Comment#category()}</b>: The category of the comment in the config screen<br>
* - <b>{@link Comment#name()}</b>: Will be used instead of the default translation key, if not empty<br>
* - <b>{@link Comment#url()}</b>: The url of the comment should link to in the config screen (none if left empty)<br>
* */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Comment {
boolean centered() default false;
String category() default "default";
String name() default "";
String url() default "";
@Deprecated String requiredMod() default "";
}
/**
* Condition Annotation<br>
* - <b>{@link Condition#requiredModId()}</b>: The id of a mod that is required to be loaded.<br>
* - <b>{@link Condition#requiredOption()}</b>: The {@link Field} which will be used to check the condition. Can also access options of other MidnightLib mods ("modid:optionName").<br>
* - <b>{@link Condition#requiredValue()}</b>: The value that {@link Condition#requiredOption()} should be set to for the condition to be met.<br>
* - <b>{@link Condition#visibleButLocked()}</b>: The behaviour to take when {@link Condition#requiredModId} is not loaded
* or {@link Condition#requiredOption()} returns a value that is not {@link Condition#requiredValue()}.<br>
* <code>true</code> – Option is visible, but not editable<br>
* <code>false</code> – Option is completely hidden
*/
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(Conditions.class)
@Target(ElementType.FIELD)
public @interface Condition {
String requiredModId() default "";
String requiredOption() default "";
String[] requiredValue() default {"true"};
boolean visibleButLocked() default false;
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Conditions {
Condition[] value();
}
}
================================================
FILE: common/src/main/java/fudge/notenoughcrashes/config/MidnightConfigListWidget.java
================================================
package fudge.notenoughcrashes.config;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gl.RenderPipelines;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.gui.widget.ClickableWidget;
import net.minecraft.client.gui.widget.ElementListWidget;
import net.minecraft.text.Text;
import java.util.List;
public class MidnightConfigListWidget extends ElementListWidget<ButtonEntry> {
public boolean renderHeaderSeparator = true;
public MidnightConfigListWidget(MinecraftClient client, int width, int height, int y, int itemHeight) {
super(client, width, height, y, itemHeight);
}
@Override
public int getScrollbarX() {
return this.width - 7;
}
@Override
public void drawHeaderAndFooterSeparators(DrawContext context) {
if (renderHeaderSeparator)
super.drawHeaderAndFooterSeparators(context);
else
context.drawTexture(RenderPipelines.GUI_TEXTURED, this.client.world == null ? Screen.FOOTER_SEPARATOR_TEXTURE : Screen.INWORLD_FOOTER_SEPARATOR_TEXTURE, this.getX(), this.getBottom(), 0, 0, this.getWidth(), 2, 32, 2);
}
public void addButton(List<ClickableWidget> buttons, Text text, EntryInfo info) {
this.addEntry(new ButtonEntry(buttons, text, info));
}
public void clear() {
this.clearEntries();
}
@Override
public int getRowWidth() {
return 10000;
}
}
================================================
FILE: common/src/main/java/fudge/notenoughcrashes/config/MidnightConfigScreen.java
================================================
package fudge.notenoughcrashes.config;
import com.google.common.collect.Lists;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.gui.tab.GridScreenTab;
import net.minecraft.client.gui.tab.Tab;
import net.minecraft.client.gui.tab.TabManager;
import net.minecraft.client.gui.tooltip.Tooltip;
import net.minecraft.client.gui.widget.*;
import net.minecraft.client.input.KeyInput;
import net.minecraft.client.resource.language.I18n;
import net.minecraft.screen.ScreenTexts;
import net.minecraft.text.Style;
import net.minecraft.text.Text;
import net.minecraft.text.TranslatableTextContent;
import net.minecraft.util.Formatting;
import net.minecraft.util.Identifier;
import net.minecraft.util.Util;
import javax.swing.*;
import javax.swing.filechooser.FileNameExtensionFilter;
import java.awt.*;
import java.util.*;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Predicate;
public class MidnightConfigScreen extends Screen {
public MidnightConfig instance;
public final String translationPrefix, modid;
public final Screen parent;
public MidnightConfigListWidget list;
public TabManager tabManager = new TabManager(a -> {}, a -> {});
public Map<String, Tab> tabs = new LinkedHashMap<>();
public Tab prevTab;
public TabNavigationWidget tabNavigation;
public ButtonWidget done;
public double scrollProgress = 0d;
public MidnightConfigScreen(Screen parent, String modid) {
super(Text.translatable(modid + ".midnightconfig.title"));
this.parent = parent;
this.modid = modid;
this.translationPrefix = modid + ".midnightconfig.";
this.instance = MidnightConfig.configInstances.get(modid);
instance.loadValuesFromJson();
MidnightConfig.entries.values().forEach(info -> {
if (Objects.equals(info.modid, modid)) {
String tabId = info.entry != null ? info.entry.category() : info.comment.category();
String name = translationPrefix + "category." + tabId;
if (!I18n.hasTranslation(name) && tabId.equals("default"))
name = translationPrefix + "title";
if (!tabs.containsKey(name)) {
info.tab = new GridScreenTab(Text.translatable(name));
tabs.put(name, info.tab);
} else info.tab = tabs.get(name);
}
});
tabNavigation = TabNavigationWidget.builder(tabManager, this.width).tabs(tabs.values().toArray(new Tab[0])).build();
tabNavigation.selectTab(0, false);
tabNavigation.init();
prevTab = tabManager.getCurrentTab();
}
// Real Time config update //
@Override
public void tick() {
super.tick();
if (prevTab != null && prevTab != tabManager.getCurrentTab()) {
prevTab = tabManager.getCurrentTab();
updateList();
list.setScrollY(0);
}
scrollProgress = list.getScrollY();
for (EntryInfo info : MidnightConfig.entries.values())
if (Objects.equals(modid, info.modid)) info.updateFieldValue();
updateButtons();
if (instance.reloadScreen) {
updateList();
instance.reloadScreen = false;
}
}
public void updateButtons() {
if (this.list == null) return;
for (ButtonEntry entry : this.list.children()) {
if (entry.buttons != null && entry.buttons.size() > 1 && entry.info.field != null) {
if (entry.buttons.get(0) instanceof ClickableWidget widget)
if (widget.isFocused() || widget.isHovered())
widget.setTooltip(entry.info.getTooltip(true));
if (entry.buttons.get(1) instanceof ButtonWidget button)
button.active = !Objects.equals(String.valueOf(entry.info.value), String.valueOf(entry.info.defaultValue)) && entry.info.conditionsMet;
}
}
}
@Override
public boolean keyPressed(KeyInput input) {
return this.tabNavigation.keyPressed(input) || super.keyPressed(input);
}
@Override
public void close() {
instance.loadValuesFromJson();
MidnightConfig.entries.values().forEach(info -> {
info.error = null;
info.value = null;
info.tempValue = null;
info.actionButton = null;
info.listIndex = 0;
info.tab = null;
info.inLimits = true;
});
Objects.requireNonNull(client).setScreen(parent);
}
@Override
public void init() {
super.init();
tabNavigation.setWidth(this.width);
tabNavigation.init();
if (tabs.size() > 1)
this.addDrawableChild(tabNavigation);
this.addDrawableChild(ButtonWidget.builder(ScreenTexts.CANCEL, button -> this.close()).dimensions(this.width / 2 - 154, this.height - 26, 150, 20).build());
done = this.addDrawableChild(ButtonWidget.builder(ScreenTexts.DONE, (button) -> {
for (EntryInfo info : MidnightConfig.entries.values())
if (info.modid.equals(modid))
info.updateFieldValue();
MidnightConfig.write(modid);
close();
}).dimensions(this.width / 2 + 4, this.height - 26, 150, 20).build());
this.list = new MidnightConfigListWidget(this.client, this.width, this.height - 57, 24, 25);
this.addSelectableChild(this.list);
updateList();
if (tabs.size() > 1)
list.renderHeaderSeparator = false;
}
public void updateList() {
this.list.clear();
instance.onTabInit(prevTab.getTitle().getContent() instanceof TranslatableTextContent translatable ?
translatable.getKey().substring(translatable.getKey().lastIndexOf('.') + 1) : prevTab.getTitle().toString(), list, this);
for (EntryInfo info : MidnightConfig.entries.values()) {
info.updateConditions();
if (!info.conditionsMet) {
boolean visibleButLocked = false;
for (MidnightConfig.Condition condition : info.conditions)
visibleButLocked |= condition.visibleButLocked();
if (!visibleButLocked) continue;
}
if (info.modid.equals(modid) && (info.tab == null || info.tab == tabManager.getCurrentTab())) {
TextIconButtonWidget resetButton = TextIconButtonWidget.builder(Text.translatable("controls.reset"), (button -> {
info.value = info.defaultValue;
info.listIndex = 0;
info.tempValue = info.toTemporaryValue();
updateList();
}), true).texture(Identifier.of("midnightlib", "icon/reset"), 12, 12).dimension(20, 20).build();
resetButton.setPosition(width - 205 + 150 + 25, 0);
if (info.function != null) {
ClickableWidget widget;
MidnightConfig.Entry e = info.entry;
if (info.function instanceof Map.Entry) { // Enums & booleans
var values = (Map.Entry<ButtonWidget.PressAction, Function<Object, Text>>) info.function;
if (info.dataType.isEnum()) {
values.setValue(value -> instance.getEnumTranslatableText(value, info));
}
widget = ButtonWidget.builder(values.getValue().apply(info.value), values.getKey()).dimensions(width - 185, 0, 150, 20).tooltip(info.getTooltip(true)).build();
} else if (e.isSlider())
widget = new MidnightSliderWidget(width - 185, 0, 150, 20, Text.of(info.tempValue), (Double.parseDouble(info.tempValue) - e.min()) / (e.max() - e.min()), info);
else
widget = new TextFieldWidget(textRenderer, width - 185, 0, 150, 20, Text.empty());
if (widget instanceof TextFieldWidget textField) {
textField.setMaxLength(e.width());
textField.setText(info.tempValue);
Predicate<String> processor = ((BiFunction<TextFieldWidget, ButtonWidget, Predicate<String>>) info.function).apply(textField, done);
textField.setTextPredicate(processor);
}
widget.setTooltip(info.getTooltip(true));
ButtonWidget cycleButton = null;
if (info.field.getType() == List.class) {
cycleButton = ButtonWidget.builder(Text.literal(String.valueOf(info.listIndex)).formatted(Formatting.GOLD), (button -> {
var values = (List<?>) info.value;
values.remove("");
info.listIndex = info.listIndex != values.size() ? info.listIndex + 1 : 0;
info.tempValue = info.listIndex != values.size() ? info.toTemporaryValue() : "";
updateList();
})).dimensions(width - 185, 0, 20, 20).tooltip(Tooltip.of(Text.translatable("midnightconfig.action.list_index", info.listIndex))).build();
}
if (e.isColor()) {
ButtonWidget colorButton = ButtonWidget.builder(Text.literal("⬛"),
button -> new Thread(() -> {
Color newColor = JColorChooser.showDialog(null, Text.translatable("midnightconfig.colorChooser.title").getString(), Color.decode(!Objects.equals(info.tempValue, "") ? info.tempValue : "#FFFFFF"));
if (newColor != null) {
info.setValue("#" + Integer.toHexString(newColor.getRGB()).substring(2));
updateList();
}
}).start()
).dimensions(width - 185, 0, 20, 20).tooltip(Tooltip.of(Text.translatable("midnightconfig.action.color_chooser"))).build();
try {
colorButton.setMessage(Text.literal("⬛").setStyle(Style.EMPTY.withColor(Color.decode(info.tempValue).getRGB())));
} catch (Exception ignored) {
}
info.actionButton = colorButton;
} else if (e.selectionMode() > -1) {
ButtonWidget explorerButton = TextIconButtonWidget.builder(Text.empty(),
button -> new Thread(() -> {
JFileChooser fileChooser = new JFileChooser(info.tempValue);
fileChooser.setFileSelectionMode(e.selectionMode());
fileChooser.setDialogType(e.fileChooserType());
fileChooser.setDialogTitle(Text.translatable(translationPrefix + info.fieldName + ".fileChooser").getString());
if ((e.selectionMode() == JFileChooser.FILES_ONLY || e.selectionMode() == JFileChooser.FILES_AND_DIRECTORIES) && Arrays.stream(e.fileExtensions()).noneMatch("*"::equals))
fileChooser.setFileFilter(new FileNameExtensionFilter(
Text.translatable(translationPrefix + info.fieldName + ".fileFilter").getString(), e.fileExtensions()));
if (fileChooser.showDialog(null, null) == JFileChooser.APPROVE_OPTION) {
info.setValue(fileChooser.getSelectedFile().getAbsolutePath());
updateList();
}
}).start(), true
).texture(Identifier.of("midnightlib", "icon/explorer"), 12, 12).dimension(20, 20).build();
explorerButton.setTooltip(Tooltip.of(Text.translatable("midnightconfig.action.file_chooser")));
explorerButton.setPosition(width - 185, 0);
info.actionButton = explorerButton;
}
List<ClickableWidget> widgets = Lists.newArrayList(widget, resetButton);
if (info.actionButton != null) {
if (Util.getOperatingSystem() == Util.OperatingSystem.OSX) info.actionButton.active = false;
widget.setWidth(widget.getWidth() - 22);
widget.setX(widget.getX() + 22);
widgets.add(info.actionButton);
}
if (cycleButton != null) {
if (info.actionButton != null) info.actionButton.setX(info.actionButton.getX() + 22);
widget.setWidth(widget.getWidth() - 22);
widget.setX(widget.getX() + 22);
widgets.add(cycleButton);
}
if (!info.conditionsMet) widgets.forEach(w -> w.active = false);
this.list.addButton(widgets, Text.translatable(info.translationKey), info);
} else this.list.addButton(List.of(), Text.translatable(info.translationKey), info);
}
list.setScrollY(scrollProgress);
updateButtons();
}
}
@Override
public void render(DrawContext context, int mouseX, int mouseY, float delta) {
super.render(context, mouseX, mouseY, delta);
this.list.render(context, mouseX, mouseY, delta);
if (tabs.size() < 2) context.drawCenteredTextWithShadow(textRenderer, title, width / 2, 10, 0xFFFFFFFF);
}
}
================================================
FILE: common/src/main/java/fudge/notenoughcrashes/config/MidnightSliderWidget.java
================================================
package fudge.notenoughcrashes.config;
import net.minecraft.client.gui.widget.SliderWidget;
import net.minecraft.text.Text;
public class MidnightSliderWidget extends SliderWidget {
private final EntryInfo info;
private final MidnightConfig.Entry e;
public MidnightSliderWidget(int x, int y, int width, int height, Text text, double value, EntryInfo info) {
super(x, y, width, height, text, value);
this.e = info.entry;
this.info = info;
}
@Override
public void updateMessage() {
this.setMessage(Text.of(info.tempValue));
}
@Override
public void applyValue() {
if (info.dataType == int.class) info.setValue(((Number) (e.min() + value * (e.max() - e.min()))).intValue());
else if (info.dataType == double.class)
info.setValue(Math.round((e.min() + value * (e.max() - e.min())) * (double) e.precision()) / (double) e.precision());
else if (info.dataType == float.class)
info.setValue(Math.round((e.min() + value * (e.max() - e.min())) * (float) e.precision()) / (float) e.precision());
}
}
================================================
FILE: common/src/main/java/fudge/notenoughcrashes/config/NecConfig.java
================================================
package fudge.notenoughcrashes.config;
public record NecConfig(boolean disableReturnToMainMenu, boolean catchInitializationCrashes,
boolean debugModIdentification, int crashLimit,boolean catchGameloopCrashes) {
public static NecConfig getCurrent() {
return new NecConfig(NecMidnightConfig.disableReturnToMainMenu, NecMidnightConfig.catchInitializationCrashes,
NecMidnightConfig.debugModIdentification, NecMidnightConfig.crashLimit, NecMidnightConfig.catchGameloop);
}
}
================================================
FILE: common/src/main/java/fudge/notenoughcrashes/config/NecMidnightConfig.java
================================================
package fudge.notenoughcrashes.config;
public class NecMidnightConfig extends MidnightConfig {
@Comment
public static Comment disableReturnToMainMenuComment1;
@Comment
public static Comment disableReturnToMainMenuComment2;
@Entry
public static boolean disableReturnToMainMenu = false;
@Comment
public static Comment catchInitializationCrashesComment1;
@Comment
public static Comment catchInitializationCrashesComment2;
@Comment
public static Comment catchInitializationCrashesComment3;
@Entry
public static boolean catchInitializationCrashes = true;
@Comment
public static Comment debugModIdentificationComment;
@Entry
public static boolean debugModIdentification = false;
@Comment
public static Comment crashLimitComment;
@Entry
public static int crashLimit = 20;
@Comment
public static Comment catchGameloopComment1;
@Comment
public static Comment catchGameloopComment2;
@Entry
public static boolean catchGameloop = true;
}
================================================
FILE: common/src/main/java/fudge/notenoughcrashes/config/OldNecConfig.java
================================================
package fudge.notenoughcrashes.config;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import fudge.notenoughcrashes.NotEnoughCrashes;
import fudge.notenoughcrashes.platform.NecPlatform;
import org.jetbrains.annotations.Nullable;
import java.io.*;
public class OldNecConfig {
/******************************
* CONFIG
*******************************/
public boolean disableReturnToMainMenu = false;
@Deprecated
public boolean deobfuscateStackTrace = true;
public boolean catchInitializationCrashes = true;
public boolean debugModIdentification = false;
public boolean forceCrashScreen = false;
public int crashLimit = 20;
public CrashUpload crashlogUpload = new CrashUpload();
public static class CrashUpload {
public CrashLogUploadDestination destination = CrashLogUploadDestination.BYTEBIN;
public String hasteUrl = "https://hastebin.com/";
public String bytebinUrl = "https://bytebin.lucko.me/";
public Gist gist = new Gist();
public Pastebin pastebin = new Pastebin();
public String customUserAgent = "";
}
public enum CrashLogUploadDestination {
GIST(3), // attempt last
HASTE(2),
PASTEBIN(null), // requires configuration
BYTEBIN(1),
CRASHY(0);
public final Integer defaultPriority;
CrashLogUploadDestination(@Nullable Integer defaultPriority) {
this.defaultPriority = defaultPriority;
}
}
public static class Gist {
public String accessToken = "";
public boolean unlisted = false;
}
public static class Pastebin {
public enum Privacy {
PUBLIC("0"), //anyone can see it, appears in recently created
UNLISTED("1"); // only people with the link can see it
// PRIVATE(2) // only you can see it (doesn't make much sense). this doesn't allow for raw download, so disabling
public final String apiValue;
Privacy(String apiValue) {
this.apiValue = apiValue;
}
}
public enum Expiry {
NEVER("N"),
TENMIN("10M"),
ONEHOUR("1H"),
ONEDAY("1D"),
ONEWEEK("1W"),
TWOWEEK("2W"),
ONEMONTH("1M"),
SIXMONTH("6M"),
ONEYEAR("1Y");
public final String pastebinExpiryKey;
Expiry(String pastebinExpiry) {
this.pastebinExpiryKey = pastebinExpiry;
}
}
public String uploadKey = "";
public Privacy privacy = Privacy.PUBLIC;
public Expiry expiry = Expiry.NEVER;
}
public static OldNecConfig instance() {
if (instance != null) {
return instance;
}
if (CONFIG_FILE.exists()) {
try {
return instance = new Gson().fromJson(new FileReader(CONFIG_FILE), OldNecConfig.class);
} catch (FileNotFoundException e) {
throw new IllegalStateException(e);
}
}
instance = new OldNecConfig();
try (FileWriter writer = new FileWriter(CONFIG_FILE)) {
GSON.toJson(instance, writer);
} catch (IOException e) {
throw new RuntimeException(e);
}
return instance;
}
private static final File CONFIG_FILE = new File(NecPlatform.instance().getConfigDirectory().toFile(), NotEnoughCrashes.MOD_ID + ".json");
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
private static OldNecConfig instance = null;
}
================================================
FILE: common/src/main/java/fudge/notenoughcrashes/gui/CrashScreen.java
================================================
package fudge.notenoughcrashes.gui;
import fudge.notenoughcrashes.config.NecConfig;
import fudge.notenoughcrashes.mixinhandlers.InGameCatcher;
import fudge.notenoughcrashes.utils.NecLocalization;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.screen.TitleScreen;
import net.minecraft.client.gui.widget.ButtonWidget;
import net.minecraft.client.gui.widget.MultilineTextWidget;
import net.minecraft.client.gui.widget.TextWidget;
import net.minecraft.client.gui.widget.Widget;
import net.minecraft.text.Text;
import net.minecraft.util.crash.CrashReport;
@Environment(EnvType.CLIENT)
public class CrashScreen extends ProblemScreen {
private static final int BODY_TEXT_COLOR = 0xD0D0D0;
public CrashScreen(CrashReport report) {
super(report);
}
@Override
public void init() {
super.init();
/* ---------- “Return to title” button ---------- */
ButtonWidget mainMenuButton = ButtonWidget.builder(
NecLocalization.translatedText("gui.toTitle"),
btn -> {
InGameCatcher.crashScreenActive = true;
MinecraftClient.getInstance().setScreen(new TitleScreen());
})
.dimensions(width / 2 - 155, height / 4 + 120 + 12, 150, 20)
.build();
if (NecConfig.getCurrent().disableReturnToMainMenu()) {
mainMenuButton.active = false;
mainMenuButton.setMessage(
NecLocalization.translatedText("notenoughcrashes.gui.disabledByConfig"));
}
addDrawableChild(mainMenuButton);
/* ---------- Text widgets ---------- */
int paragraphStartX = width / 2;
int y = height / 4 - 51; // title is 40 px above the old body start
// Title
addDrawableChild(centeredText(paragraphStartX, y,
Text.translatable("notenoughcrashes.crashscreen.title"), 0xFFFFFF));
// Body copy – keeps the original spacing
y = addBodyLine(paragraphStartX, y + 40, 18, "notenoughcrashes.crashscreen.summary");
y = addBodyLine(paragraphStartX, y, 11, "notenoughcrashes.crashscreen.paragraph1.line1");
y += 11; // blank row between paragraphs
y = addBodyLine(paragraphStartX, y, 11, "notenoughcrashes.crashscreen.paragraph2.line1");
y = addBodyLine(paragraphStartX, y, 9, "notenoughcrashes.crashscreen.paragraph2.line2");
/* crash‑report file path */
String fileName = getFileNameString(); // helper in ProblemScreen
y = addBodyLine(paragraphStartX, y, 11, fileName, 0x00FF00);
y = addBodyLine(paragraphStartX, y, 12, "notenoughcrashes.crashscreen.paragraph3.line1");
y = addBodyLine(paragraphStartX, y, 9, "notenoughcrashes.crashscreen.paragraph3.line2");
y = addBodyLine(paragraphStartX, y, 9, "notenoughcrashes.crashscreen.paragraph3.line3");
addBodyLine(paragraphStartX, y, 9, "notenoughcrashes.crashscreen.paragraph3.line4");
}
/* --------------------------------------------------------------------- */
/* Helpers */
/* --------------------------------------------------------------------- */
/** Convenience factory for a centred `TextWidget`. */
private TextWidget centeredText(int centreX, int y, Text text, int color) {
var w = new TextWidget(text, textRenderer);
w.setX(centreX - textRenderer.getWidth(text.getString()) / 2);
w.setY(y);
w.setTextColor(color);
return w;
}
/** Overload for translation keys. */
private TextWidget centeredText(int centreX, int y, String translationKey, int color) {
return centeredText(centreX, y, Text.translatable(translationKey), color);
}
/**
* Add a body line, returning the new Y coordinate so that callers can keep
* the original spacing logic.
*/
private int addBodyLine(int centreX, int currentY, int offset, String keyOrLiteral) {
return addBodyLine(centreX, currentY, offset, keyOrLiteral, BODY_TEXT_COLOR);
}
/**
* Add a body line, returning the new Y coordinate so that callers can keep
* the original spacing logic.
*/
private int addBodyLine(int centreX, int currentY, int offset, String keyOrLiteral, int color) {
int y = currentY + offset;
addDrawableChild(centeredText(centreX, y, keyOrLiteral, color));
return y;
}
}
================================================
FILE: common/src/main/java/fudge/notenoughcrashes/gui/InitErrorScreen.java
================================================
package fudge.notenoughcrashes.gui;
import fudge.notenoughcrashes.utils.NecLocalization;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.widget.ButtonWidget;
import net.minecraft.client.gui.widget.TextWidget;
import net.minecraft.text.Text;
import net.minecraft.util.crash.CrashReport;
@Environment(EnvType.CLIENT)
public class InitErrorScreen extends ProblemScreen {
private static final int BODY_TEXT_COLOR = 0xD0D0D0;
public InitErrorScreen(CrashReport report) {
super(report);
}
/* ------------------------------------------------------------------ */
/* Screen setup */
/* ------------------------------------------------------------------ */
@Override
public void init() {
super.init();
/* ---------- “Quit” button ---------- */
ButtonWidget exitButton = ButtonWidget.builder(
Text.translatable("menu.quit"),
btn -> System.exit(-1))
.dimensions(width / 2 - 155, height / 4 + 120 + 12, 150, 20)
.build();
addDrawableChild(exitButton);
/* ---------- Text widgets ---------- */
int paragraphStartX = width / 2;
int y = height / 4 - 40; // same baseline offset used in CrashScreen
// Title
addDrawableChild(centeredText(paragraphStartX, y,
Text.translatable("notenoughcrashes.initerrorscreen.title"), 0xFFFFFF));
// Body – replicate original vertical gaps
y = addBodyLine(paragraphStartX, y + 40, 0, "notenoughcrashes.initerrorscreen.summary");
y = addBodyLine(paragraphStartX, y, 18, "notenoughcrashes.crashscreen.paragraph1.line1");
y = addBodyLine(paragraphStartX, y, 22, "notenoughcrashes.crashscreen.paragraph2.line1");
y = addBodyLine(paragraphStartX, y, 9, "notenoughcrashes.crashscreen.paragraph2.line2");
/* crash‑report file path */
String fileName = getFileNameString(); // helper in ProblemScreen
y = addBodyLine(paragraphStartX, y, 11, fileName, 0x00FF00);
y = addBodyLine(paragraphStartX, y, 12, "notenoughcrashes.initerrorscreen.paragraph3.line1");
y = addBodyLine(paragraphStartX, y, 9, "notenoughcrashes.initerrorscreen.paragraph3.line2");
y = addBodyLine(paragraphStartX, y, 9, "notenoughcrashes.initerrorscreen.paragraph3.line3");
addBodyLine(paragraphStartX, y, 9, "notenoughcrashes.initerrorscreen.paragraph3.line4");
}
/* ------------------------------------------------------------------ */
/* Helpers (shared style with CrashScreen) */
/* ------------------------------------------------------------------ */
/** Returns a centred `TextWidget` positioned by *pixel* centre, not widget width. */
private TextWidget centeredText(int centreX, int y, Text text, int color) {
var w = new TextWidget(text, textRenderer);
w.setX(centreX - textRenderer.getWidth(text.getString()) / 2);
w.setY(y);
w.setTextColor(color);
return w;
}
/** Overload that accepts a translation‑key or literal string. */
private TextWidget centeredText(int centreX, int y, String keyOrLiteral, int color) {
return centeredText(centreX, y, Text.translatable(keyOrLiteral), color);
}
/**
* Adds a body line, preserving the spacing pattern used in CrashScreen.
* Returns the Y coordinate of the line that was just placed.
*/
private int addBodyLine(int centreX, int currentY, int offset, String keyOrLiteral) {
return addBodyLine(centreX, currentY, offset, keyOrLiteral, BODY_TEXT_COLOR);
}
private int addBodyLine(int centreX, int currentY, int offset, String keyOrLiteral, int color) {
int y = currentY + offset;
addDrawableChild(centeredText(centreX, y, keyOrLiteral, color));
return y;
}
}
================================================
FILE: common/src/main/java/fudge/notenoughcrashes/gui/ProblemScreen.java
================================================
package fudge.notenoughcrashes.gui;
import fudge.notenoughcrashes.NotEnoughCrashes;
import fudge.notenoughcrashes.platform.CommonModMetadata;
import fudge.notenoughcrashes.stacktrace.ModIdentifier;
import fudge.notenoughcrashes.upload.LegacyCrashLogUpload;
import fudge.notenoughcrashes.utils.NecLocalization;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.minecraft.client.gui.Click;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.gui.widget.ButtonWidget;
import net.minecraft.client.gui.widget.TextWidget;
import net.minecraft.text.ClickEvent;
import net.minecraft.text.MutableText;
import net.minecraft.text.Text;
import net.minecraft.util.Util;
import net.minecraft.util.crash.CrashReport;
import net.minecraft.util.crash.ReportType;
import java.net.URI;
import java.nio.file.Path;
import java.util.*;
@Environment(EnvType.CLIENT)
public abstract class ProblemScreen extends Screen {
private static final Set<String> IGNORED_MODS = new HashSet<>(Arrays.asList(
"minecraft", "fabricloader", "loadcatcher", "jumploader", "quilt_loader", "forge", "notenoughcrashes"
));
protected CrashReport report;
private String uploadedCrashLink = null;
protected int xLeft = Integer.MAX_VALUE;
protected int xRight = Integer.MIN_VALUE;
protected int yTop = Integer.MAX_VALUE;
protected int yBottom = Integer.MIN_VALUE;
protected int x;
protected int y;
protected ProblemScreen(CrashReport report) {
super(Text.of(""));
this.report = report;
}
private Text getSuspectedModsText() {
Set<CommonModMetadata> suspectedMods = ModIdentifier.getSuspectedModsOf(report);
// Minecraft exists and basically any stack trace, and loader exists in any launch,
// it's better not to include them in the list of mods.
suspectedMods.removeIf(mod -> IGNORED_MODS.contains(mod.id()));
if (suspectedMods.isEmpty()) {
return NecLocalization.translatedText("notenoughcrashes.crashscreen.noModsErrored");
}
return suspectedMods.stream()
.sorted(Comparator.comparing(CommonModMetadata::name))
.map(mod -> {
String issuesPage = mod.issuesPage();
MutableText modText = Text.literal(mod.name());
if (issuesPage != null) {
modText.styled(style -> style.withClickEvent(new ClickEvent.OpenUrl(URI.create(issuesPage))));
}
return modText;
})
.reduce((existing, next) -> existing.append(Text.of(", ")).append(next))
.get();
}
private void addSuspectedModsWidget() {
var widget = new TextWidget(getSuspectedModsText(),textRenderer);
widget.setX(width / 2 - textRenderer.getWidth(getSuspectedModsText().getString()) / 2);
widget.setTextColor(0xE0E000);
widget.setY(y + 29);
addDrawableChild(widget);
}
private void handleLegacyLinkClick(ButtonWidget buttonWidget) {
try {
if (uploadedCrashLink == null) {
uploadedCrashLink = LegacyCrashLogUpload.upload(report.asString(ReportType.MINECRAFT_CRASH_REPORT));
}
Util.getOperatingSystem().open(uploadedCrashLink);
} catch (Throwable e) {
NotEnoughCrashes.getLogger().error("Exception when crash menu button clicked:", e);
buttonWidget.setMessage(NecLocalization.translatedText("notenoughcrashes.gui.failed"));
buttonWidget.active = false;
}
}
@Override
public void init() {
addDrawableChild(
ButtonWidget.builder(
NecLocalization.translatedText("notenoughcrashes.gui.getLink")
, this::handleLegacyLinkClick)
.dimensions(width / 2 - 155 + 160, height / 4 + 120 + 12, 150, 20)
.build()
);
x = width / 2 - 155;
y = height / 4;
addSuspectedModsWidget();
}
@Override
public boolean mouseClicked(Click click, boolean doubled) {
if (x >= xLeft && x <= xRight && y >= yTop && y <= yBottom) {
Path file = report.getFile();
if (file != null) {
Util.getOperatingSystem().open(file);
}
}
return super.mouseClicked(click, doubled);
}
@Override
public boolean shouldCloseOnEsc() {
return false;
}
String getFileNameString() {
return report.getFile() != null ? "\u00A7n" + report.getFile().getFileName()
: NecLocalization.localize("notenoughcrashes.crashscreen.reportSaveFailed");
}
@Override
public void render(DrawContext context, int mouseX, int mouseY, float delta) {
super.render(context, mouseX, mouseY, delta);
}
}
================================================
FILE: common/src/main/java/fudge/notenoughcrashes/mixinhandlers/EntryPointCatcher.java
================================================
package fudge.notenoughcrashes.mixinhandlers;
import fudge.notenoughcrashes.NotEnoughCrashes;
import fudge.notenoughcrashes.gui.InitErrorScreen;
import fudge.notenoughcrashes.stacktrace.CrashUtils;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.minecraft.MinecraftVersion;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.util.Window;
import net.minecraft.util.crash.CrashReport;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class EntryPointCatcher {
private static CrashReport crashReport = null;
public static boolean crashedDuringStartup() {
return crashReport != null;
}
private static final Logger LOGGER = LogManager.getLogger(NotEnoughCrashes.NAME + " Entry Points");
@Environment(EnvType.CLIENT)
public static void handleEntryPointError(Throwable e) {
crashReport = CrashReport.create(e, "Initializing game");
crashReport.addElement("Initialization");
MinecraftClient.addSystemDetailsToCrashReport(null, null, MinecraftVersion.create().name(), null, crashReport);
CrashUtils.outputClientReport(crashReport);
// Make GL shuttup about any GL error that occurred
Window.acceptError((integer, stringx) -> {
});
}
@Environment(EnvType.CLIENT)
public static void displayInitErrorScreen() {
try {
MinecraftClient.getInstance().setScreen(new InitErrorScreen(crashReport));
} catch (Throwable t) {
CrashReport additionalReport = CrashReport.create(t, "Displaying init error screen");
LOGGER.error("An uncaught exception occured while displaying the init error screen, making normal report instead", t);
CrashUtils.outputClientReport(additionalReport);
System.exit(additionalReport.getFile() != null ? -1 : -2);
}
}
}
================================================
FILE: common/src/main/java/fudge/notenoughcrashes/mixinhandlers/InGameCatcher.java
================================================
package fudge.notenoughcrashes.mixinhandlers;
import fudge.notenoughcrashes.NotEnoughCrashes;
import fudge.notenoughcrashes.config.NecConfig;
import fudge.notenoughcrashes.gui.CrashScreen;
import fudge.notenoughcrashes.patches.MinecraftClientAccess;
import fudge.notenoughcrashes.stacktrace.CrashUtils;
import fudge.notenoughcrashes.utils.GlUtil;
import fudge.notenoughcrashes.utils.NecLocalization;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.screen.MessageScreen;
import net.minecraft.text.Text;
import net.minecraft.util.crash.CrashReport;
import net.minecraft.util.profiler.DummyRecorder;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.Queue;
public class InGameCatcher {
private static final Logger LOGGER = LogManager.getLogger(NotEnoughCrashes.NAME + " In Game Crashes");
private static int clientCrashCount = 0;
private static int serverCrashCount = 0;
public static boolean crashScreenActive = false;
public static void handleClientCrash(CrashReport report) {
clientCrashCount++;
addInfoToCrash(report);
resetStates();
// boolean reported = report.getCause() instanceof CrashException;
// LOGGER.fatal(reported ? "Reported" : "Unreported" + " exception thrown!", report.getCause());
displayCrashScreen(report, clientCrashCount, true);
// Continue game loop
getClient().run();
}
private static void resetStates() {
GlUtil.resetState();
// StateManager.resetStates();
resetModState();
resetCriticalGameState();
}
public static void cleanupBeforeMinecraft() {
if (getClient().getNetworkHandler() != null) {
// Fix: Close the connection to avoid receiving packets from old server
// when playing in another world (MC-128953)
getClient().getNetworkHandler().getConnection().disconnect(Text.of(String.format("[%s] Client crashed", NotEnoughCrashes.NAME)));
}
getClient().disconnect(new MessageScreen(NecLocalization.translatedText("menu.savingLevel")), false);
}
// Sometimes the game fails to reset this so we make sure it happens ourselves
private static void resetCriticalGameState() {
MinecraftClient client = getClient();
// Turn off profiler because it will crash the game if the world is closed
if (((MinecraftClientAccess) client).getRecorder().isActive()) {
client.toggleDebugProfiler(null);
((MinecraftClientAccess) client).setRecorder(DummyRecorder.INSTANCE);
}
client.player = null;
client.world = null;
var server = client.getServer();
if (server != null) server.stop(true);
}
private static void resetModState() {
// NotEnoughCrashesApi.permanentDisposers.forEach(Runnable::run);
// NotEnoughCrashesApi.oneTimeDisposers.forEach(Runnable::run);
// NotEnoughCrashesApi.oneTimeDisposers.clear();
}
public static void handleServerCrash(CrashReport report) {
serverCrashCount++;
addInfoToCrash(report);
displayCrashScreen(report, serverCrashCount, false);
}
private static MinecraftClient getClient() {
return MinecraftClient.getInstance();
}
public static void addInfoToCrash(CrashReport report) {
report.getSystemDetailsSection().addSection("Client Crashes Since Restart", () -> String.valueOf(clientCrashCount));
report.getSystemDetailsSection().addSection("Integrated Server Crashes Since Restart", () -> String.valueOf(serverCrashCount));
}
public static void displayCrashScreen(CrashReport report, int crashCount, boolean clientCrash) {
crashScreenActive = true;
try {
if (EntryPointCatcher.crashedDuringStartup()) {
throw new IllegalStateException("Could not initialize startup crash screen");
}
if (crashCount > NecConfig.getCurrent().crashLimit()) {
throw new IllegalStateException("The game has crashed an excessive amount of times");
}
CrashUtils.outputReport(report, clientCrash);
// Vanilla does this when switching to main menu but not our custom crash screen
// nor the out of memory screen (see https://bugs.mojang.com/browse/MC-128953)
getClient().inGameHud.getChatHud().clear(true);
// Display the crash screen
getClient().setScreen(new CrashScreen(report));
} catch (Throwable t) {
crashScreenActive = false;
// The crash screen has crashed. Report it normally instead.
LOGGER.error("An uncaught exception occured while displaying the crash screen, making normal report instead", t);
getClient().printCrashReport(report);
System.exit(report.getFile() != null ? -1 : -2);
}
}
}
================================================
FILE: common/src/main/java/fudge/notenoughcrashes/mixins/MixinCrashReport.java
================================================
package fudge.notenoughcrashes.mixins;
import fudge.notenoughcrashes.stacktrace.ModIdentifier;
import net.minecraft.util.SystemDetails;
import net.minecraft.util.crash.CrashReport;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.stream.Collectors;
@Mixin(value = CrashReport.class, priority = 500)
public abstract class MixinCrashReport {
@Shadow
@Final
private SystemDetails systemDetailsSection;
private CrashReport getThis() {
return (CrashReport) (Object) this;
}
/**
* @reason Adds a list of mods which may have caused the crash to the report.
*/
@Inject(method = "addDetails", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/SystemDetails;writeTo(Ljava/lang/StringBuilder;)V"))
private void beforeSystemDetailsAreWritten(CallbackInfo ci) {
systemDetailsSection.addSection("Suspected Mods", () -> {
try {
var suspectedMods = ModIdentifier.getSuspectedModsOf(getThis());
if (!suspectedMods.isEmpty()) {
return suspectedMods.stream()
.map((mod) -> mod.name() + " (" + mod.id() + ")")
.collect(Collectors.joining(", "));
} else return "None";
} catch (Throwable e) {
return ExceptionUtils.getStackTrace(e).replace("\t", " ");
}
});
}
}
================================================
FILE: common/src/main/java/fudge/notenoughcrashes/mixins/MixinTileEntity.java
================================================
package fudge.notenoughcrashes.mixins;
import net.minecraft.block.entity.BlockEntity;
import net.minecraft.util.crash.CrashReportSection;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(value = BlockEntity.class, priority = 10000)
public class MixinTileEntity {
private boolean noNBT = false;
@SuppressWarnings("UnreachableCode")
@Inject(method = "populateCrashReport", at = @At("TAIL"))
private void onPopulateCrashReport(CrashReportSection section, CallbackInfo ci) {
if (!noNBT) {
noNBT = true;
var self = (BlockEntity) (Object) this;
var world = self.getWorld();
if (world != null) {
section.add("Block Entity NBT", () -> self.createNbt(world.getRegistryManager()).toString());
}
noNBT = false;
}
}
}
================================================
FILE: common/src/main/java/fudge/notenoughcrashes/mixins/client/MixinKeyboard.java
================================================
package fudge.notenoughcrashes.mixins.client;
import fudge.notenoughcrashes.mixinhandlers.InGameCatcher;
import net.minecraft.client.Keyboard;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(Keyboard.class)
public class MixinKeyboard {
/**
* pollDebugCrash() keeps crashing the game when we display the crash screen, infinitely times over,
* so we need to stop it from crashing after it has done its job just once.
*/
@Inject(method = "pollDebugCrash()V", at = @At("HEAD"), cancellable = true)
public void pollDebugCrashDontCrashInfinitely(CallbackInfo ci) {
if (InGameCatcher.crashScreenActive) ci.cancel();
}
}
================================================
FILE: common/src/main/java/fudge/notenoughcrashes/mixins/client/MixinMinecraftClient.java
================================================
package fudge.notenoughcrashes.mixins.client;
import fudge.notenoughcrashes.NotEnoughCrashes;
import fudge.notenoughcrashes.gui.InitErrorScreen;
import fudge.notenoughcrashes.mixinhandlers.EntryPointCatcher;
import fudge.notenoughcrashes.mixinhandlers.InGameCatcher;
import fudge.notenoughcrashes.patches.MinecraftClientAccess;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.screen.Screen;
import net.minecraft.util.crash.CrashReport;
import net.minecraft.util.profiler.Recorder;
import net.minecraft.util.thread.ReentrantThreadExecutor;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.ModifyArg;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.Queue;
import java.util.function.Supplier;
//TODO: infinite repeating crashes in different cases, need to go over the crash cases and solve them one by one.
@Mixin(MinecraftClient.class)
public abstract class MixinMinecraftClient extends ReentrantThreadExecutor<Runnable> implements MinecraftClientAccess {
@Shadow
@Nullable
private Supplier<CrashReport> crashReportSupplier;
@Shadow
private Recorder recorder;
@Shadow
public void printCrashReport(CrashReport report) {
}
@Override
public Recorder getRecorder() {
return recorder;
}
@Override
public void setRecorder(Recorder recorder) {
this.recorder = recorder;
}
public MixinMinecraftClient(String string_1) {
super(string_1);
}
/**
* If the game has crashed, we set the screen to the init crash screen, but then Minecraft sets the screen back
* to the title screen. We want to prevent that, to keep the screen to be the InitCrashScreen
*/
@Inject(method = "setScreen(Lnet/minecraft/client/gui/screen/Screen;)V", at = @At("HEAD"), cancellable = true)
private void setScreenDontResetCrashScreen(Screen screen, CallbackInfo ci) {
if (EntryPointCatcher.crashedDuringStartup() && !(screen instanceof InitErrorScreen)) ci.cancel();
}
@Inject(method = "run()V", at = @At("HEAD"))
private void beforeRun(CallbackInfo ci) {
if (EntryPointCatcher.crashedDuringStartup()) EntryPointCatcher.displayInitErrorScreen();
}
// @Inject(method = "run()V", at = @At(value = "FIELD", target = "Lnet/minecraft/client/MinecraftClient;crashReportSupplier:Ljava/util/function/Supplier;"))
// private void onRunLoop(CallbackInfo ci) {
//
// }
@Inject(method = "printCrashReport()V", at = @At("HEAD"))
private void onCheckGameCrashed(CallbackInfo ci) {
if (!NotEnoughCrashes.enableGameloopCatching()) return;
if (this.crashReportSupplier != null) {
NotEnoughCrashes.logDebug("Handling run loop crash");
InGameCatcher.handleServerCrash(crashReportSupplier.get());
// Causes the run loop to keep going
crashReportSupplier = null;
}
}
// Can't capture arg in inject so captured here
@ModifyArg(method = "run()V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/MinecraftClient;printCrashReport(Lnet/minecraft/util/crash/CrashReport;)V", ordinal = 0))
private CrashReport atTheEndOfFirstCatchBeforePrintingCrashReport(CrashReport report) {
if (!NotEnoughCrashes.enableGameloopCatching()) return report;
NotEnoughCrashes.logDebug("Handling client game loop try/catch crash in first catch block");
// we MUST use the report passed as parameter, because the field one only gets assigned in integrated server crashes.
InGameCatcher.handleClientCrash(report);
return report;
}
// Can't capture arg in inject so captured here
@ModifyArg(method = "run()V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/MinecraftClient;printCrashReport(Lnet/minecraft/util/crash/CrashReport;)V", ordinal = 1))
private CrashReport atTheEndOfSecondCatchBeforePrintingCrashReport(CrashReport report) {
if (!NotEnoughCrashes.enableGameloopCatching()) return report;
NotEnoughCrashes.logDebug("Handling client game loop try/catch crash in second catch block");
// we MUST use the report passed as parameter, because the field one only gets assigned in integrated server crashes.
InGameCatcher.handleClientCrash(report);
return report;
}
// // Prevent calling printCrashReport which is not needed
// @Inject(method = "run()V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/MinecraftClient;printCrashReport(Lnet/minecraft/util/crash/CrashReport;)V"), cancellable = true)
// private void cancelRunLoopAfterCrash(CallbackInfo ci) {
// if (NotEnoughCrashes.enableGameloopCatching()) ci.cancel();
// }
@Inject(method = "cleanUpAfterCrash()V", at = @At("HEAD"))
private void beforeCleanUpAfterCrash(CallbackInfo info) {
if (NotEnoughCrashes.enableGameloopCatching()) {
InGameCatcher.cleanupBeforeMinecraft();
}
}
//String levelName, LevelStorage.Session session, ResourcePackManager dataPackManager, SaveLoader saveLoader
//TODO: test this: maybe we don't need this mixin anymore?
// /**
// * Prevent the integrated server from exiting in the case it crashed
// */
// @Redirect(method = "startIntegratedServer(Lnet/minecraft/world/level/storage/LevelStorage$Session;Lnet/minecraft/resource/ResourcePackManager;Lnet/minecraft/server/SaveLoader;Z)V",
// at = @At(value = "INVOKE", target = "Lnet/minecraft/client/MinecraftClient;printCrashReport(Lnet/minecraft/util/crash/CrashReport;)V"))
// private void redirectPrintCrashReport(CrashReport report) {
// if (!NotEnoughCrashes.enableGameloopCatching()) printCrashReport(report);
// }
/**
* Forge only: Prevent the integrated server from exiting in the case it crashed in another case
*/
@SuppressWarnings({"MixinAnnotationTarget", "UnresolvedMixinReference"})
@Redirect(method = "doLoadLevel(Ljava/lang/String;Lnet/minecraft/util/registry/DynamicRegistryManager$Impl;Ljava/util/function/Function;Lcom/mojang/datafixers/util/Function4;ZLnet/minecraft/client/MinecraftClient$WorldLoadAction;Z)V",
at = @At(value = "INVOKE", target = "Lnet/minecraft/client/MinecraftClient;printCrashReport(Lnet/minecraft/util/crash/CrashReport;)V"),
require = 0)
private void redirectForgePrintCrashReport(CrashReport report) {
if (!NotEnoughCrashes.enableGameloopCatching()) printCrashReport(report);
}
}
================================================
FILE: common/src/main/java/fudge/notenoughcrashes/mixins/client/MixinMinecraftServer.java
================================================
package fudge.notenoughcrashes.mixins.client;
//
///**
// * Mixin to stop the integrated server from ticking when the game has crashed
// */
//@Mixin(MinecraftServer.class)
//public class MixinMinecraftServer {
// @Inject(method = "Lnet/minecraft/server/MinecraftServer;shouldKeepTicking()Z", at = @At("HEAD"), cancellable = true)
// public void shouldKeepTickingStopTickOnCrash() {
//
// }
//}
================================================
FILE: common/src/main/java/fudge/notenoughcrashes/mixins/client/MixinMinecraftServerClientOnly.java
================================================
package fudge.notenoughcrashes.mixins.client;
import fudge.notenoughcrashes.NotEnoughCrashes;
import net.minecraft.server.MinecraftServer;
import net.minecraft.util.crash.CrashReport;
import net.minecraft.util.crash.ReportType;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
import java.io.File;
import java.nio.file.Path;
/**
* Only applied to client side because we aim to change the functionality of the integrated server (and not the dedicated one)
*/
@Mixin(MinecraftServer.class)
public class MixinMinecraftServerClientOnly {
/**
* We write the log anyway using CrashUtils.outputReport in
* {@link fudge.notenoughcrashes.mixinhandlers.InGameCatcher#displayCrashScreen(CrashReport, int, boolean)}
*/
@Redirect(method = "runServer()V", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/crash/CrashReport;writeToFile(Ljava/nio/file/Path;Lnet/minecraft/util/crash/ReportType;)Z"))
private boolean disableIntegratedServerWriteToFileOnCrash(CrashReport instance, Path path, ReportType type) {
if (NotEnoughCrashes.enableGameloopCatching()) return true;
else return instance.writeToFile(path, type);
}
}
================================================
FILE: common/src/main/java/fudge/notenoughcrashes/patches/MinecraftClientAccess.java
================================================
package fudge.notenoughcrashes.patches;
import net.minecraft.util.profiler.Recorder;
public interface MinecraftClientAccess {
Recorder getRecorder();
void setRecorder(Recorder recorder);
}
================================================
FILE: common/src/main/java/fudge/notenoughcrashes/platform/CommonModMetadata.java
================================================
package fudge.notenoughcrashes.platform;
import org.jetbrains.annotations.Nullable;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
public record CommonModMetadata(
String id, String name, @Nullable String issuesPage, @Nullable List<String> authors, Path rootPath
) {
public static final CommonModMetadata STUB = new CommonModMetadata(
"", "UNKNOWN", null, null, Paths.get("")
);
}
================================================
FILE: common/src/main/java/fudge/notenoughcrashes/platform/ModsByLocation.java
================================================
package fudge.notenoughcrashes.platform;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.net.URI;
import java.nio.file.Path;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
public class ModsByLocation {
private final Map<String, Set<CommonModMetadata>> locationToMod;
private static String stringify(Set<CommonModMetadata> mods) {
return String.format("[%s]", mods.stream().map(CommonModMetadata::name).collect(Collectors.joining(",")));
}
@Override
public String toString() {
return "Mods By Location: {\n" + locationToMod.entrySet().stream()
.map((kv) -> "\t'" + kv.getKey() + "' -> " + stringify(kv.getValue()))
.collect(Collectors.joining(",\n")) + "\n}";
}
public ModsByLocation(Map<Path, Set<CommonModMetadata>> locationToMod) {
Map<String, Set<CommonModMetadata>> mods = new HashMap<>();
locationToMod.forEach((path, mod) -> {
mods.put(normalizePathString(path.toUri().toString()), mod);
});
this.locationToMod = mods;
}
public Set<CommonModMetadata> get(URI path) {
return locationToMod.get(normalizePathString(path.toString()));
}
@Nullable
public Set<CommonModMetadata> get(Path path) {
return get(path.toUri());
}
@NotNull
public Set<CommonModMetadata> getOrEmpty(Path path) {
Set<CommonModMetadata> mods = get(path);
return mods != null ? mods : Collections.emptySet();
}
private static String normalizePathString(String path) {
// Remove 'union:/' that forge has
String noUnion = removePrefix(path, "union:/");
// Remove scheme marker
String noScheme = removeAndBefore(noUnion, "//");
// Remove '%2370!' that forge has, example:
// union:/C:/Users/natan/.gradle/caches/fabric-loom/1.17.1/net.fabricmc.yarn.1_17_1.1.17.1+build.61-v2-forge-1.17.1-37.0.69/forge-1.17.1-37.0.69-minecraft-mapped.jar%2371!
// We use 'removeLastPercentSymbol' instead of removing everything after last occurrence of '%' so it works with spaces as well
// (the last space will be 'deleted', but that doesn't matter for our purposes)
String noPercent = removeLastPercentSymbol(noScheme);
// Remove trailing '/' and '!'
return removeSuffix(removeSuffix(noPercent, "/"), "!");
}
// e.g. converts 'asdf%123fwefw' to 'asdffwefw'
private static String removeLastPercentSymbol(String str) {
int toRemovePos = str.lastIndexOf("%");
if (toRemovePos == -1) return str;
int i = toRemovePos + 1;
for (; i < str.length(); i++) {
// Travel until we reach a non-digit character
if (!Character.isDigit(str.charAt(i))) break;
}
return str.substring(0, toRemovePos) + str.substring(i);
}
private static String removeSuffix(String str, String suffix) {
return str.endsWith(suffix) ? str.substring(0, str.length() - suffix.length()) : str;
}
private static String removePrefix(String str, String suffix) {
return str.startsWith(suffix) ? str.substring(suffix.length()) : str;
}
private static String removeAndBefore(String str, String toRemove) {
int toRemovePos = str.lastIndexOf(toRemove);
if (toRemovePos == -1) return str;
else return str.substring(toRemovePos + toRemove.length());
}
private static String removeAndAfter(String str, String toRemove) {
int toRemovePos = str.lastIndexOf(toRemove);
if (toRemovePos == -1) return str;
else return str.substring(0, toRemovePos);
}
}
================================================
FILE: common/src/main/java/fudge/notenoughcrashes/platform/NecPlatform.java
================================================
package fudge.notenoughcrashes.platform;
import org.jetbrains.annotations.Nullable;
import java.io.InputStream;
import java.nio.file.Path;
import java.util.List;
public interface NecPlatform {
static NecPlatform instance() {
return NecPlatformStorage.INSTANCE_SET_ONLY_BY_SPECIFIC_PLATFORMS_VERY_EARLY;
}
boolean isForge();
boolean isModLoaded(String modId);
ModsByLocation getModsAtLocationsInDisk();
Path getGameDirectory();
Path getConfigDirectory();
boolean isDevelopmentEnvironment();
/**
* Returns null if no such resource exists at resources/relativePath
*/
@Nullable
InputStream getResource(Path relativePath);
/**
* Get be multiple metadatas because forge supports having multiple mods under one jar
*/
List<CommonModMetadata> getModMetadatas(String modId);
List<CommonModMetadata> getAllMods();
boolean modContainsFile(CommonModMetadata mod, String path);
default boolean irisExists() {
return false;
}
boolean isClient();
default boolean isClientEnv() {
return isClient();
}
}
================================================
FILE: common/src/main/java/fudge/notenoughcrashes/platform/NecPlatformStorage.java
================================================
package fudge.notenoughcrashes.platform;
public class NecPlatformStorage {
public static NecPlatform INSTANCE_SET_ONLY_BY_SPECIFIC_PLATFORMS_VERY_EARLY = null;
}
================================================
FILE: common/src/main/java/fudge/notenoughcrashes/stacktrace/CrashUtils.java
================================================
package fudge.notenoughcrashes.stacktrace;
import fudge.notenoughcrashes.NotEnoughCrashes;
import fudge.notenoughcrashes.platform.NecPlatform;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.client.MinecraftClient;
import net.minecraft.util.crash.CrashReport;
import net.minecraft.util.crash.ReportType;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.File;
import java.nio.file.Path;
import java.text.SimpleDateFormat;
import java.util.Date;
public final class CrashUtils {
// private static boolean isClient;
//
// static {
// try {
// isClient = MinecraftClient.getInstance() != null;
// } catch (NoClassDefFoundError e) {
// isClient = false;
// }
// }
public static void outputClientReport(CrashReport report) {
outputReport(report,true);
}
// We don't use the Mojang printCrashReport because it calls System.exit(), lol
public static void outputReport(CrashReport report, boolean isClient) {
try {
if (report.getFile() == null) {
String reportName = "crash-";
reportName += new SimpleDateFormat("yyyy-MM-dd_HH.mm.ss").format(new Date());
reportName += isClient && MinecraftClient.getInstance().isOnThread() ? "-client" : "-server";
reportName += ".txt";
Path reportsDir = NecPlatform.instance().getGameDirectory().resolve("crash-reports");
Path reportFile = reportsDir.resolve(reportName);
report.writeToFile(reportFile, ReportType.MINECRAFT_CRASH_REPORT);
}
} catch (Throwable e) {
NotEnoughCrashes.getLogger().fatal("Failed saving report", e);
}
NotEnoughCrashes.getLogger().fatal("Minecraft ran into a problem! " + (report.getFile() != null ? "Report saved to: " + report.getFile() :
"Crash report could not be saved.") + "\n" +
report.asString(ReportType.MINECRAFT_CRASH_REPORT));
}
}
================================================
FILE: common/src/main/java/fudge/notenoughcrashes/stacktrace/ModIdentifier.java
================================================
package fudge.notenoughcrashes.stacktrace;
import fudge.notenoughcrashes.NotEnoughCrashes;
import fudge.notenoughcrashes.config.NecConfig;
import fudge.notenoughcrashes.platform.CommonModMetadata;
import fudge.notenoughcrashes.platform.ModsByLocation;
import fudge.notenoughcrashes.platform.NecPlatform;
import net.minecraft.util.crash.CrashReport;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.extensibility.IMixinConfig;
import org.spongepowered.asm.mixin.extensibility.IMixinInfo;
import org.spongepowered.asm.mixin.transformer.ClassInfo;
import org.spongepowered.asm.mixin.transformer.meta.MixinMerged;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.CodeSource;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Supplier;
public final class ModIdentifier {
private static final Map<CrashReport, Set<CommonModMetadata>> suspectedModsCache = new HashMap<>();
private static final Map<IMixinConfig, Set<CommonModMetadata>> mixinConfigToModsCache = new HashMap<>();
public static Set<CommonModMetadata> getSuspectedModsOf(CrashReport report) {
return suspectedModsCache.computeIfAbsent(report, (ignored) -> identifyFromStacktrace(report.getCause()));
}
@NotNull
private static Set<CommonModMetadata> identifyFromStacktrace(Throwable e) {
Set<CommonModMetadata> mods = new HashSet<>();
// Include suppressed exceptions too
visitChildrenThrowables(e, throwable -> {
for (var newMod : identifyFromThrowable(throwable)) {
if (mods.stream().noneMatch(mod -> mod.id().equals(newMod.id()))) {
mods.add(newMod);
}
}
});
return mods;
}
private static void visitChildrenThrowables(Throwable e, Consumer<Throwable> visitor) {
visitor.accept(e);
for (Throwable child : e.getSuppressed()) visitChildrenThrowables(child, visitor);
}
private static Set<CommonModMetadata> identifyFromThrowable(Throwable e) {
ModsByLocation modMap = NecPlatform.instance().getModsAtLocationsInDisk();
Set<String> involvedClasses = new LinkedHashSet<>();
Set<IMixinInfo> involvedMixins = new LinkedHashSet<>();
while (e != null) {
for (StackTraceElement element : e.getStackTrace()) {
involvedClasses.add(element.getClassName());
IMixinInfo mixinInfo = getMixinInfo(element);
if (mixinInfo != null) involvedMixins.add(mixinInfo);
}
e = e.getCause();
}
Set<CommonModMetadata> mods = new LinkedHashSet<>();
for (String className : involvedClasses) {
Set<CommonModMetadata> classMods = identifyFromClass(className, modMap);
mods.addAll(classMods);
}
for (IMixinInfo mixinName : involvedMixins) {
Set<CommonModMetadata> mixinMods = identifyFromMixin(mixinName);
mods.addAll(mixinMods);
}
debug(modMap::toString);
return mods;
}
private static final boolean FORCE_DEBUG = false;
private static void debug(Supplier<String> message) {
if (FORCE_DEBUG || NecConfig.getCurrent().debugModIdentification()) NotEnoughCrashes.getLogger().info(message.get());
}
@NotNull
private static Set<CommonModMetadata> identifyFromClass(String className, ModsByLocation modMap) {
// Skip identification for Mixin, one's mod copy of the library is shared with all other mods
if (className.startsWith("org.spongepowered.asm.mixin.")) {
debug(() -> "Ignoring class " + className + " for identification because it is a mixin class");
return Collections.emptySet();
}
try {
// Get the URL of the class
Class<?> clazz = Class.forName(className);
CodeSource codeSource = clazz.getProtectionDomain().getCodeSource();
if (codeSource == null) {
debug(() -> "Ignoring class " + className + " for identification because the code source could not be found");
return Collections.emptySet(); // Some internal native sun classes
}
URL url = codeSource.getLocation();
if (url == null) {
NotEnoughCrashes.getLogger().warn("Failed to identify mod for " + className);
return Collections.emptySet();
}
// Get the mod containing that class
Set<CommonModMetadata> mods = getModsAt(Paths.get(url.toURI()), modMap);
if (NecConfig.getCurrent().debugModIdentification() && !mods.isEmpty()){
debug(() -> "Successfully placed blame of '" + className + "' on '"
+ mods.stream().findFirst().get().name() + "'");
}
return mods;
} catch (URISyntaxException | ClassNotFoundException | NoClassDefFoundError e) {
debug(() -> "Ignoring class " + className + " for identification because an error occurred");
if (NecConfig.getCurrent().debugModIdentification()) {
e.printStackTrace();
}
return Collections.emptySet(); // we cannot do it
}
}
@Nullable
private static MixinMerged findMixinMerged(StackTraceElement element) {
try {
Class<?> clazz = Class.forName(element.getClassName());
// Walk through methods because we don't know parameter types
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
if (method.getName().equals(element.getMethodName())) {
MixinMerged mixinMerged = method.getAnnotation(MixinMerged.class);
if (mixinMerged != null) {
return mixinMerged;
}
}
}
} catch (ClassNotFoundException | NoClassDefFoundError ignored) {
debug(() -> "Ignoring class " + element.getClassName() + " for mixin identification because an error occurred");
}
return null;
}
@NotNull
private static Set<CommonModMetadata> identifyFromMixin(IMixinInfo mixin) {
return mixinConfigToModsCache.computeIfAbsent(mixin.getConfig(), config -> {
String mixinFileName = config.getName();
Set<CommonModMetadata> modsWithMixinFile = new LinkedHashSet<>();
for (CommonModMetadata mod : NecPlatform.instance().getAllMods()) {
if (NecPlatform.instance().modContainsFile(mod, mixinFileName)) {
modsWithMixinFile.add(mod);
}
}
return modsWithMixinFile;
});
}
@Nullable
private static IMixinInfo getMixinInfo(StackTraceElement element) {
MixinMerged mixinMerged = findMixinMerged(element);
if (mixinMerged != null) {
// Mixin does a great job of obscuring mixins - see the "Reflection" javadoc for more info
ClassInfo classInfo = ClassInfo.forName(mixinMerged.mixin().replace('.', '/'));
if (classInfo != null) {
return Reflection.getMixinInfo(classInfo);
}
}
return null;
}
@NotNull
private static Set<CommonModMetadata> getModsAt(Path path, ModsByLocation modMap) {
Set<CommonModMetadata> mod = modMap.get(path);
if (mod != null) return mod;
else if (NecPlatform.instance().isDevelopmentEnvironment()) {
// For some reason, in dev, the mod being tested has the 'resources' folder as the origin instead of the 'classes' folder.
String resourcesPathString = path.toString().replace("\\", "/")
// Make it work with Architectury as well
.replace("common/build/classes/java/main", "fabric/build/resources/main")
.replace("common/build/classes/kotlin/main", "fabric/build/resources/main")
.replace("classes/java/main", "resources/main")
.replace("classes/kotlin/main", "resources/main");
Path resourcesPath = Paths.get(resourcesPathString);
return modMap.getOrEmpty(resourcesPath);
} else {
debug(() -> "Mod at path '" + path.toAbsolutePath() + "' is at fault," +
" but it could not be found in the map of mod paths: " /*+ modMap*/);
return Collections.emptySet();
}
}
/**
* Mixin is rather protective of its internal mixin structures.
* <p>
* Mixin has some useful classes:
* <ul>
* <li>{@link ClassInfo} - Represents a class</li>
* <li>{@link IMixinInfo} - Represents a mixin</li>
* </ul>
* The issue arises when we have a {@link ClassInfo} of a mixin and want to get its {@link IMixinInfo}.
* The ClassInfo class has a {@link ClassInfo#mixin mixin} field, used to link an IMixinInfo to a mixin's ClassInfo.
* However, this field is private and there is no getter for it. We would have to use reflection to get it.
* <p>
* Another option would be to get the ClassInfo of the mixin's target class to get mixins into that class.
* This has a problem, though, since Mixin likes to claim that mixins are not applied and therefore not accessible with the public {@link ClassInfo#getAppliedMixins()} method.
* Since it only has a getter for applied mixins and not all mixins, reflection is the only option.
* <p>
* Therefore, the easiest option is to use reflection to get the IMixinInfo from a mixin's ClassInfo.
*/
private static class Reflection {
static final Field classInfoMixin;
static {
try {
classInfoMixin = ClassInfo.class.getDeclaredField("mixin");
classInfoMixin.setAccessible(true);
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
@Nullable
static IMixinInfo getMixinInfo(ClassInfo classInfo) {
try {
return (IMixinInfo) classInfoMixin.get(classInfo);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
}
================================================
FILE: common/src/main/java/fudge/notenoughcrashes/upload/CrashyUpload.java
================================================
package fudge.notenoughcrashes.upload;
import com.google.gson.Gson;
import fudge.notenoughcrashes.NotEnoughCrashes;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.zip.GZIPOutputStream;
@SuppressWarnings("ConstantValue")
public class CrashyUpload {
private enum CrashyMode {
LOCAL, BETA, RELEASE
}
private static final CrashyMode CRASHY_MODE = CrashyMode.RELEASE;
private static final String crashyDomain = CRASHY_MODE == CrashyMode.RELEASE ? "crashy.net" :
CRASHY_MODE == CrashyMode.BETA ? "beta.crashy.net" :
"localhost:80";
private static final String http = CRASHY_MODE == CrashyMode.LOCAL ? "http" :"https";
private static final Path CRASH_CODES_PATH = NotEnoughCrashes.DIRECTORY.resolve("Uploaded Crash logs.txt");
public static CompletableFuture<String> uploadToCrashy(String text) throws IOException {
try {
var prefix = http + "://" + crashyDomain;
return java11PostAsync(prefix + "/uploadCrash", gzip(text)).thenApplyAsync(response -> {
int statusCode = response.statusCode();
String responseBody = response.body();
return switch (statusCode) {
case HttpURLConnection.HTTP_OK -> {
UploadCrashSuccess responseObject = new Gson().fromJson(responseBody, UploadCrashSuccess.class);
try {
rememberCrashCode(responseObject.crashId, responseObject.deletionKey);
} catch (IOException e) {
NotEnoughCrashes.getLogger().error("Could not remember crash code when uploading crash " + responseObject.crashId, e);
}
yield responseObject.crashyUrl;
}
case HttpURLConnection.HTTP_BAD_REQUEST -> throw new UploadToCrashyError.InvalidCrash();
case HttpURLConnection.HTTP_ENTITY_TOO_LARGE -> throw new UploadToCrashyError.TooLarge();
default -> throw new IllegalStateException("Unexpected status code when uploading to crashy: " + statusCode + " message: " + responseBody);
};
});
} catch (InterruptedException/* | ExecutionException*/ e) {
throw new RuntimeException(e);
}
}
public static String uploadToCrashySync(String text) throws IOException, ExecutionException, InterruptedException {
return uploadToCrashy(text).get();
}
private static CompletableFuture<HttpResponse<String>> java11PostAsync(String url, byte[] body) throws InterruptedException {
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create(url))
.setHeader("content-type", "text/plain")
.setHeader("content-encoding", "gzip")
.POST(HttpRequest.BodyPublishers.ofByteArray(body))
.build();
return client.sendAsync(request, HttpResponse.BodyHandlers.ofString());
}
private static byte[] gzip(String string) throws IOException {
try (var baos = new ByteArrayOutputStream()) {
try (var gzos = new GZIPOutputStream(baos)) {
gzos.write(string.getBytes(StandardCharsets.UTF_8));
}
return baos.toByteArray();
}
}
private static void rememberCrashCode(String id, String code) throws IOException {
NotEnoughCrashes.ensureDirectoryExists();
final String oldCodes;
if (Files.exists(CRASH_CODES_PATH)) {
oldCodes = Files.readString(CRASH_CODES_PATH);
} else {
oldCodes = "";
}
Files.writeString(CRASH_CODES_PATH, oldCodes + id + ": " + code + "\n");
}
static class UploadCrashSuccess {
String crashId;
String deletionKey;
String crashyUrl;
}
}
================================================
FILE: common/src/main/java/fudge/notenoughcrashes/upload/LegacyCrashLogUpload.java
================================================
package fudge.notenoughcrashes.upload;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.annotations.SerializedName;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Map;
public final class LegacyCrashLogUpload {
private static String GIST_ACCESS_TOKEN_PART_1() {
return "dc07dacff0c2cf84f706";
}
private static String GIST_ACCESS_TOKEN_PART_2() {
return "8ac0fd6a757d53b81233";
}
// I don't think there's any security problem because the token can only upload gists,
// but Github will revoke the token as soon as it sees it, so we trick it by splitting the token into 2.
private static final String GIST_ACCESS_TOKEN = GIST_ACCESS_TOKEN_PART_1() + GIST_ACCESS_TOKEN_PART_2();
private static class GistPost {
@SerializedName("public")
public boolean isPublic;
public Map<String, GistFile> files;
public GistPost(boolean isPublic, Map<String, GistFile> files) {
this.isPublic = isPublic;
this.files = files;
}
}
private static class GistFile {
public String content;
public GistFile(String content) {
this.content = content;
}
}
public static String upload(String text) throws IOException {
return uploadToByteBin(text);
// return upload(text, new HashSet<>());
}
// public static String upload(String text, Set<NecConfig.CrashLogUploadDestination> failedUploadDestinations) throws IOException {
// final var uploadDestination = chooseUploadDestination(failedUploadDestinations);
//
// try {
// return switch (uploadDestination) {
// case CRASHY -> CrashyUpload.uploadToCrashySync(text);
// case GIST -> uploadToGist(text);
// case HASTE -> uploadToHaste(text);
// case PASTEBIN -> uploadToPasteBin(text);
// case BYTEBIN -> uploadToByteBin(text);
// };
// } catch (IOException e) {
// NotEnoughCrashes.getLogger().error("Uploading to " + uploadDestination + " failed, using another destination as fallback.", e);
//
// // If uploading failed, attempt the other destination options
// failedUploadDestinations.add(uploadDestination);
// return upload(text, failedUploadDestinations);
// } catch (ExecutionException | InterruptedException e) {
// throw new RuntimeException(e);
// }
// }
//
// private static NecConfig.CrashLogUploadDestination chooseUploadDestination(Set<NecConfig.CrashLogUploadDestination> failedUploadTypes) throws IOException {
// if (failedUploadTypes.isEmpty()) return NecConfig.getCurrent().crashlogUpload.destination;
//
// // When priority is null, the destination cannot be used as a fallback.
// var selectedDestination = Arrays.stream(NecConfig.CrashLogUploadDestination.values())
// // When priority is null, the destination cannot be used as a fallback.
// .filter(destination -> destination.defaultPriority != null && !failedUploadTypes.contains(destination)).min(Comparator.comparingInt(destination -> destination.defaultPriority));
//
// return selectedDestination.orElseThrow(() -> new IOException("All upload destinations failed!"));
// }
//
//
// /**
// * @return The link of the gist
// */
// private static String uploadToGist(String text) throws IOException {
// final var config = NecConfig.getCurrent().crashlogUpload.gist;
// String configUploadKey = config.accessToken;
// String uploadKey = GIST_ACCESS_TOKEN;
// HttpPost post = new HttpPost("https://api.github.com/gists");
//
// String fileName = "crash.txt";
// post.addHeader("Authorization", "token " + uploadKey);
//
// GistPost body = new GistPost(!config.unlisted, new HashMap<>() {{
// put(fileName, new GistFile(text));
// }});
// post.setEntity(createStringEntity(new Gson().toJson(body)));
//
// final String customUserAgent = NecConfig.getCurrent().crashlogUpload.customUserAgent;
// if (!customUserAgent.isEmpty()) {
// post.setHeader("User-Agent", customUserAgent);
// }
//
// try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
// CloseableHttpResponse response = httpClient.execute(post);
// String responseString = EntityUtils.toString(response.getEntity());
// JsonObject responseJson = new Gson().fromJson(responseString, JsonObject.class);
// return responseJson.getAsJsonObject("files").getAsJsonObject(fileName).getAsJsonPrimitive("raw_url").getAsString();
// }
//
// }
//
// private static String uploadToHaste(String str) throws IOException {
// String url = NecConfig.getCurrent().crashlogUpload.hasteUrl;
// String customUserAgent = NecConfig.getCurrent().crashlogUpload.customUserAgent;
// HttpPost post = new HttpPost(url + "documents");
// post.setEntity(createStringEntity(str));
//
// if (!customUserAgent.isEmpty()) {
// post.setHeader("User-Agent", customUserAgent);
// }
//
// try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
// CloseableHttpResponse response = httpClient.execute(post);
// String responseString = EntityUtils.toString(response.getEntity());
// JsonObject responseJson = new Gson().fromJson(responseString, JsonObject.class);
// String hasteKey = responseJson.getAsJsonPrimitive("key").getAsString();
// return url + "raw/" + hasteKey;
// }
//
// }
//
// private static String uploadToPasteBin(String text) throws IOException {
// HttpPost post = new HttpPost("https://pastebin.com/api/api_post.php");
// final var config = NecConfig.getCurrent().crashlogUpload.pastebin;
// String pastebinUploadKey = config.uploadKey;
// String pastebinPrivacy = config.privacy.apiValue;
// String pastebinExpiryKey = config.expiry.pastebinExpiryKey;
//
// List<NameValuePair> params = Arrays.asList(new BasicNameValuePair("api_dev_key", pastebinUploadKey), new BasicNameValuePair("api_option", "paste"), // to create
// new BasicNameValuePair("api_paste_code", text), new BasicNameValuePair("api_paste_name", "crash.txt"), // mirroring gist
// new BasicNameValuePair("api_paste_format", "yaml"), // hl.js auto detects mc crashes as this
// new BasicNameValuePair("api_paste_expire_date", pastebinExpiryKey), new BasicNameValuePair("api_paste_private", pastebinPrivacy));
// post.setEntity(new UrlEncodedFormEntity(params, "UTF-8"));
//
// try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
// CloseableHttpResponse response = httpClient.execute(post);
// String responseString = EntityUtils.toString(response.getEntity());
// // returns a normal pastebin url, like https://pastebin.com/xxxxxxxxx
// // inserting raw afer the .com works to return the raw content
// return responseString.replace("https://pastebin.com/", "https://pastebin.com/raw/");
// }
// }
//
private static String uploadToByteBin(String text) throws IOException {
String url = "https://bytebin.lucko.me/";
HttpPost post = new HttpPost(url + "post");
String userAgent = String.join(" ", Arrays.toString(post.getHeaders("User-Agent"))).concat(" NotEnoughCrashes");
post.setHeader("User-Agent", userAgent);
post.addHeader("Content-Type", "text/plain");
post.setEntity(createStringEntity(text));
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
CloseableHttpResponse response = httpClient.execute(post);
String responseString = EntityUtils.toString(response.getEntity());
JsonObject responseJson = new Gson().fromJson(responseString, JsonObject.class);
String bytebinKey = responseJson.getAsJsonPrimitive("key").getAsString();
return url + bytebinKey;
}
}
private static StringEntity createStringEntity(String text) {
return new StringEntity(text, StandardCharsets.UTF_16);
}
}
================================================
FILE: common/src/main/java/fudge/notenoughcrashes/upload/UploadToCrashyError.java
================================================
package fudge.notenoughcrashes.upload;
public abstract class UploadToCrashyError extends RuntimeException {
public UploadToCrashyError(String message) {
super(message);
}
public static class InvalidCrash extends UploadToCrashyError {
public InvalidCrash() {
super("Uploaded crash log is invalid");
}
}
public static class TooLarge extends UploadToCrashyError {
public TooLarge() {
super("Uploaded crash log is too large");
}
}
}
================================================
FILE: common/src/main/java/fudge/notenoughcrashes/utils/GlUtil.java
================================================
package fudge.notenoughcrashes.utils;
import com.mojang.blaze3d.opengl.GlStateManager;
import com.mojang.blaze3d.systems.RenderSystem;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL12;
import org.lwjgl.opengl.GL13;
import org.lwjgl.opengl.GL14;
public class GlUtil {
/**
* This method goes over all the RenderSystem methods Minecraft calls, and resets them.
* This is to prevent situations when Minecraft calls for example enableX, then crashes, and then it doesn't call disableX itself.
* In that case, we call disableX ourselves so rendering will keep working properly.
* Sometimes, Minecraft does disableX and then enableX. In that case we need to do enableX ourselves.
*/
public static void resetState() {
// Method calls are in the order they are declared in the Minecraft source.
// Reset texture
GlStateManager._bindTexture(0);
// Reset depth
GlStateManager._disableDepthTest();
// RenderSystem.disableScissor();
GlStateManager._depthFunc(513);
GlStateManager._depthMask(true);
// Reset blend mode
GlStateManager._disableBlend();
// RenderSystem.blendFunc(GlStateManager.SrcFactor.ONE, GlStateManager.DstFactor.ZERO);
// RenderSystem.blendFuncSeparate(GlStateManager.SrcFactor.ONE, GlStateManager.DstFactor.ZERO, GlStateManager.SrcFactor.ONE, GlStateManager.DstFactor.ZERO);
// RenderSystem.blendEquation(GL14.GL_FUNC_ADD);
GlStateManager._enableCull();
GlStateManager._polygonMode(GL11.GL_FRONT, GL11.GL_FILL);
GlStateManager._polygonMode(GL11.GL_BACK, GL11.GL_FILL);
// Reset polygon offset
GlStateManager._polygonOffset(0.0F, 0.0F);
GlStateManager._disablePolygonOffset();
// Reset color logic
GlStateManager._disableColorLogicOp();
// RenderSystem.logicOp(GlStateManager.LogicOp.COPY);
// Disable lightmap
GlStateManager._activeTexture(GL13.GL_TEXTURE1);
GlStateManager._activeTexture(GL13.GL_TEXTURE0);
// Reset texture parameters
GlStateManager._texParameter(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR);
GlStateManager._texParameter(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_NEAREST_MIPMAP_LINEAR);
GlStateManager._texParameter(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_REPEAT);
GlStateManager._texParameter(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL11.GL_REPEAT);
GlStateManager._texParameter(GL11.GL_TEXTURE_2D, GL12.GL_TEXTURE_MAX_LEVEL, 1000);
GlStateManager._texParameter(GL11.GL_TEXTURE_2D, GL12.GL_TEXTURE_MAX_LOD, 1000);
GlStateManager._texParameter(GL11.GL_TEXTURE_2D, GL12.GL_TEXTURE_MIN_LOD, -1000);
GlStateManager._texParameter(GL11.GL_TEXTURE_2D, GL14.GL_TEXTURE_LOD_BIAS, 0);
GlStateManager._colorMask(true, true, true, true);
// RenderSystem.clearDepth(1.0D);
RenderSystem.lineWidth(1.0F);
// RenderSystem.clearDepth(1.0D);
GlStateManager._enableDepthTest();
GlStateManager._depthFunc(515);
GlStateManager._enableCull();
GL11.glDisable(GL11.GL_SCISSOR_TEST);
}
}
================================================
FILE: common/src/main/java/fudge/notenoughcrashes/utils/NecLocalization.java
================================================
package fudge.notenoughcrashes.utils;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import fudge.notenoughcrashes.NotEnoughCrashes;
import fudge.notenoughcrashes.platform.NecPlatform;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.resource.language.I18n;
import net.minecraft.text.Text;
import org.apache.commons.io.IOUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
/**
* We use custom localization to not rely on fabric-resource-loader
* Only use this for NEC translation string, not for Minecraft ones
*/
public class NecLocalization {
private static final String DEFAULT_LANGUAGE_CODE = "en_us";
private static final boolean useCustomLocalization = (!NecPlatform.instance().isForge()
&& !NecPlatform.instance().isModLoaded("fabric-resource-loader-v0"))
// In edge cases where stuff doesn't get localized properly, make sure we use custom localization.
|| I18n.translate("notenoughcrashes.crashscreen.title").equals("notenoughcrashes.crashscreen.title");
public static String localize(String translationKey) {
if (useCustomLocalization) return localizeCustom(translationKey);
else return I18n.translate(translationKey);
}
@NotNull
private static String localizeCustom(String translationKey) {
String currentLanguageCode = getCurrentLanguageCode();
String translationForChosenLanguage = localizeCustom(translationKey, currentLanguageCode);
if (translationForChosenLanguage != null) return translationForChosenLanguage;
else {
String englishTranslation = localizeCustom(translationKey, DEFAULT_LANGUAGE_CODE);
return englishTranslation == null ? translationKey : englishTranslation;
}
}
@Nullable
private static String localizeCustom(String translationKey, String languageCode) {
LanguageTranslations translations = storedLanguages.computeIfAbsent(
languageCode, (ignored) -> loadLanguage(languageCode)
);
return translations.get(translationKey);
}
public static Text translatedText(String translationKey) {
if (useCustomLocalization && translationKey.startsWith(NotEnoughCrashes.MOD_ID)) return Text.of(localize(translationKey));
else return Text.translatable(translationKey);
}
@SuppressWarnings("ClassCanBeRecord")
private static class LanguageTranslations {
private final Map<String, String> translations;
private LanguageTranslations(Map<String, String> translations) {
this.translations = translations;
}
@Nullable String get(String translationKey) {
return translations.get(translationKey);
}
}
private static final Map<String, LanguageTranslations> storedLanguages = new HashMap<>();
private static String getCurrentLanguageCode() {
return MinecraftClient.getInstance().getLanguageManager().getLanguage();
}
private static LanguageTranslations loadLanguage(String code) {
Map<String, String> translations;
try (InputStream localizations = getLocalizations(code)) {
if (localizations == null) {
translations = new HashMap<>();
NotEnoughCrashes.logDebug("No localization for language code: " + code);
} else {
String content = IOUtils.toString(localizations, StandardCharsets.UTF_8); /*Java11.readString();*/
translations = parseTranslations(content);
}
} catch (IOException e) {
NotEnoughCrashes.getLogger().error("Could not load translations: ", e);
translations = new HashMap<>();
}
return new LanguageTranslations(translations);
}
@Nullable
private static InputStream getLocalizations(String code) throws IOException {
Path relativePath = Paths.get("assets", NotEnoughCrashes.MOD_ID, "lang", code + ".json");
return NecPlatform.instance().getResource(relativePath);
}
private static final Gson gson = new Gson();
private static Map<String, String> parseTranslations(String raw) {
JsonObject jsonObject = gson.fromJson(raw, JsonObject.class);
Map<String, String> translations = new HashMap<>();
for (var child : jsonObject.entrySet()) {
translations.put(child.getKey(), child.getValue().getAsString());
}
return translations;
}
}
================================================
FILE: common/src/main/java/fudge/notenoughcrashes/utils/SystemExitBlock.java
================================================
package fudge.notenoughcrashes.utils;//package fudge.notenoughcrashes.utils;
//
//import fudge.notenoughcrashes.config.NecConfig;
//
//import java.security.Permission;
//
//public class SystemExitBlock {
// public static void forbidSystemExitCall() {
// final SecurityManager securityManager = new SecurityManager() {
// public void checkPermission(Permission permission) {
// if (NecConfig.getCurrent().forceCrashScreen() && permission.getName().startsWith("exitVM")) {
// throw new SystemExitBlockedException("An attempt was made to forcefully close the game with no stack trace (see stack trace)." +
// " Not Enough Crashes made the game simply crash instead since the forceCrashScreen option is enabled.");
// }
// }
// };
// System.setSecurityManager(securityManager);
// }
//}
================================================
FILE: common/src/main/java/fudge/notenoughcrashes/utils/SystemExitBlockedException.java
================================================
package fudge.notenoughcrashes.utils;
class SystemExitBlockedException extends SecurityException {
public SystemExitBlockedException(String s) {
super(s);
}
}
================================================
FILE: common/src/main/resources/assets/notenoughcrashes/lang/de_de.json
================================================
{
"notenoughcrashes.gui.getLink": "Link erhalten",
"notenoughcrashes.gui.failed": "[Fehlgeschlagen]",
"notenoughcrashes.crashscreen.title": "Minecraft ist abgestürzt!",
"notenoughcrashes.crashscreen.summary": "Minecraft ist ein Problem unterlaufen und ist abgestürzt.",
"notenoughcrashes.crashscreen.paragraph1.line1": "Folgende Modifikation(en) wurde(n) als mögliche Ursache identifiziert:",
"notenoughcrashes.crashscreen.paragraph2.line1": "Klicke auf einen beliebigen Mod in der Liste, um auf die Website zu gelangen.",
"notenoughcrashes.crashscreen.paragraph2.line2": "Ein Bericht wurde erstellt und kann hier gefunden werden (Klick):",
"notenoughcrashes.crashscreen.reportSaveFailed": "[FEHLER BEIM SPEICHERN, SIEHE LOG]",
"notenoughcrashes.crashscreen.paragraph3.line1": "Klicke auf die Schaltfläche \"Link erhalten\", um ihn im Browser zu",
"notenoughcrashes.crashscreen.paragraph3.line2": "öffnen. Du bist aufgefordert, diesen Bericht an den Modhersteller",
"notenoughcrashes.crashscreen.paragraph3.line3": "zu senden, um ihm zu helfen, das Problem zu beheben. Da Not Enough Crashes",
"notenoughcrashes.crashscreen.paragraph3.line4": "installiert ist, kannst du trotz des Absturzes weiterspielen."
}
================================================
FILE: common/src/main/resources/assets/notenoughcrashes/lang/en_us.json
================================================
{
"notenoughcrashes.gui.getLink": "Show crash log",
"notenoughcrashes.gui.uploadToCrashy": "Upload to Crashy",
"notenoughcrashes.gui.loadingCrashyUpload": "Uploading...",
"notenoughcrashes.gui.failed": "[Failed, Report to NEC]",
"notenoughcrashes.gui.keepPlaying": "Keep playing",
"notenoughcrashes.gui.restart": "Restart Minecraft",
"notenoughcrashes.gui.disabledByConfig": "Disabled by config",
"notenoughcrashes.crashscreen.title": "Minecraft crashed!",
"notenoughcrashes.crashscreen.summary": "Minecraft ran into a problem and crashed.",
"notenoughcrashes.crashscreen.paragraph1.line1": "The following mod(s) have been identified as potential causes:",
"notenoughcrashes.crashscreen.unknownCause": "Unknown",
"notenoughcrashes.crashscreen.identificationErrored": "[Error identifying, report to Not Enough Crashes]",
"notenoughcrashes.crashscreen.noModsErrored": "No Mods. (A mod could still be indirectly causing the crash.)",
"notenoughcrashes.crashscreen.paragraph2.line1": "Click any mod in the list to go to their website for support. A",
"notenoughcrashes.crashscreen.paragraph2.line2": "report has been generated, and can be found here:",
"notenoughcrashes.crashscreen.reportSaveFailed": "[Error saving report, see log]",
"notenoughcrashes.crashscreen.paragraph3.line1": "Click the \"Show crash log\" button to open it in your browser. You're",
"notenoughcrashes.crashscreen.paragraph3.line2": "encouraged to send this report to the mod's author to help",
"notenoughcrashes.crashscreen.paragraph3.line3": "them fix the issue. Since Not Enough Crashes is installed, you can",
"notenoughcrashes.crashscreen.paragraph3.line4": "keep playing despite the crash.",
"notenoughcrashes.initerrorscreen.title": "Minecraft failed to start!",
"notenoughcrashes.initerrorscreen.summary": "An error during startup prevented Minecraft from starting",
"notenoughcrashes.initerrorscreen.paragraph3.line1": "Click the \"Show crash log\" button to open it in your browser. You're",
"notenoughcrashes.initerrorscreen.paragraph3.line2": "encouraged to send this report to the mod's author to help",
"notenoughcrashes.initerrorscreen.paragraph3.line3": "them fix the issue. Unfortunately, it is not possible to keep",
"notenoughcrashes.initerrorscreen.paragraph3.line4": "loading Minecraft due to this error.",
"notenoughcrashes.midnightconfig.disableReturnToMainMenuComment1": "If true, the \"Return To Main Menu\" button will be disabled when crashing,",
"notenoughcrashes.midnightconfig.disableReturnToMainMenuComment2": "meaning you cannot recover from a crash.",
"notenoughcrashes.midnightconfig.disableReturnToMainMenu": "Disable Return To Main Menu",
"notenoughcrashes.midnightconfig.catchInitializationCrashesComment1": "(Fabric Only) If false, the game will close normally when it fails to initialize,",
"notenoughcrashes.midnightconfig.catchInitializationCrashesComment2": "and will not display a special crash screen.",
"notenoughcrashes.midnightconfig.catchInitializationCrashesComment3": "This will be always disabled (even if true) when certain mods are installed.",
"notenoughcrashes.midnightconfig.catchInitializationCrashes": "Catch Initialization Crashes",
"notenoughcrashes.midnightconfig.debugModIdentificationComment": "If true, additional info will be logged for the mod developer.",
"notenoughcrashes.midnightconfig.debugModIdentification": "Debug Mod Identification",
"notenoughcrashes.midnightconfig.crashLimitComment": "How many times NEC will try to prevent the game from closing in one session.",
"notenoughcrashes.midnightconfig.crashLimit": "Crash Limit",
"notenoughcrashes.midnightconfig.catchGameloopComment1": "If false, NEC won't attempt to prevent the game from closing when it crashes.",
"notenoughcrashes.midnightconfig.catchGameloopComment2": "This will be always disabled (even if the config is set to true) when Iris is installed.",
"notenoughcrashes.midnightconfig.catchGameloop": "Catch Gameplay Crashes",
"notenoughcrashes.midnightconfig.title": "Not Enough Crashes Configuration"
}
================================================
FILE: common/src/main/resources/assets/notenoughcrashes/lang/et_ee.json
================================================
{
"notenoughcrashes.gui.getLink": "Hangi link",
"notenoughcrashes.gui.failed": "[Ebaõnnestus]",
"notenoughcrashes.gui.keepPlaying": "Jätka mängimist",
"notenoughcrashes.gui.restart": "Taaskäivita Minecraft",
"notenoughcrashes.gui.disabledByConfig": "Keelatud seadistustes",
"notenoughcrashes.crashscreen.title": "Minecraft jooksis kokku!",
"notenoughcrashes.crashscreen.summary": "Minecraftil esines viga ning see jooksis kokku.",
"notenoughcrashes.crashscreen.paragraph1.line1": "Järgnev(ad) mod(id) on tuvastatud tõenäolise põhjustajana:",
"notenoughcrashes.crashscreen.unknownCause": "Tundmatu",
"notenoughcrashes.crashscreen.identificationErrored": "[Tuvastamisel esines viga, teata Not Enough Crashes'ile]",
"notenoughcrashes.crashscreen.noModsErrored": "Mitte ükski mod. (Mõni mod võis siiski kaudselt krahhi põhjustada.)",
"notenoughcrashes.crashscreen.paragraph2.line1": "Klõpsa mistahes modile siin nimekirjas, et minna toe saamiseks selle",
"notenoughcrashes.crashscreen.paragraph2.line2": "kodulehele. Genereeritud on ka raport, selle leiab siit (klõpsa):",
"notenoughcrashes.crashscreen.reportSaveFailed": "[Raporti salvestamisel esines viga, vaata logi]",
"notenoughcrashes.crashscreen.paragraph3.line1": "Klõpsa nuppu \"Hangi link\", et avada see oma brauseris.",
"notenoughcrashes.crashscreen.paragraph3.line2": "Soovitame selle raporti modi autorile saata, et aidata tal",
"notenoughcrashes.crashscreen.paragraph3.line3": "seda viga parandada. Kuna sul on paigaldatud Not Enough Crashes,",
"notenoughcrashes.crashscreen.paragraph3.line4": "saad sa krahhist hoolimata edasi mängida.",
"notenoughcrashes.initerrorscreen.title": "Minecraft ei suutnud käivituda!",
"notenoughcrashes.initerrorscreen.summary": "Käivitusel esinev viga segas Minecrafti käivitumist",
"notenoughcrashes.initerrorscreen.paragraph3.line1": "Klõpsa nuppu \"Hangi link\", et avada see oma brauseris.",
"notenoughcrashes.initerrorscreen.paragraph3.line2": "Soovitame selle raporti modi autorile saata, et aidata tal",
"notenoughcrashes.initerrorscreen.paragraph3.line3": "seda viga parandada. Kahjuks takistab see viga Minecrafti",
"notenoughcrashes.initerrorscreen.paragraph3.line4": "edasi laadimise jätkamist."
}
================================================
FILE: common/src/main/resources/assets/notenoughcrashes/lang/fr_fr.json
================================================
{
"notenoughcrashes.gui.getLink": "Obtenir un lien",
"notenoughcrashes.gui.failed": "[Échoué]",
"notenoughcrashes.gui.keepPlaying": "Continuer à jouer",
"notenoughcrashes.crashscreen.title": "Minecraft a crashé!",
"notenoughcrashes.crashscreen.summary": "Minecraft a rencontré un problème et a crashé.",
"notenoughcrashes.crashscreen.paragraph1.line1": "Ces mod(s) ont été identifiés comme des causes potentielles:",
"notenoughcrashes.crashscreen.unknownCause": "Cause inconnue",
"notenoughcrashes.crashscreen.identificationErrored": "[Problème dans l'identification, raportez à Not Enough Crashes]",
"notenoughcrashes.crashscreen.paragraph2.line1": "Cliquez n'importe quel mod dans la liste pour aller a leur page",
"notenoughcrashes.crashscreen.paragraph2.line2": "pour du support. Un raport a été génére et se trouve ici (cliquez):",
"notenoughcrashes.crashscreen.reportSaveFailed": "[Erreur durant la sauvegarde, voir le log]",
"notenoughcrashes.crashscreen.paragraph3.line1": "Cliquez le bouton « Obtenir un lien » pour l'ouvrir dans votre",
"notenoughcrashes.crashscreen.paragraph3.line2": "navigateur web. Vous êtez encouragé de l'envoyer à l'auteur du",
"notenoughcrashes.crashscreen.paragraph3.line3": "mod pour l'aider à résoudre le problême. Vu que TooManyCrashes est",
"notenoughcrashes.crashscreen.paragraph3.line4": "installé, vous pouvez continuer a jouer malgré le crash."
}
================================================
FILE: common/src/main/resources/assets/notenoughcrashes/lang/ja_jp.json
================================================
{
"notenoughcrashes.gui.getLink": "クラッシュログを表示",
"notenoughcrashes.gui.uploadToCrashy": "Crashyにアップロード",
"notenoughcrashes.gui.loadingCrashyUpload": "アップロード中...",
"notenoughcrashes.gui.failed": "[失敗しました。NECに報告してください。]",
"notenoughcrashes.gui.keepPlaying": "プレイを続ける",
"notenoughcrashes.gui.restart": "Minecraftを再起動",
"notenoughcrashes.gui.disabledByConfig": "設定により無効",
"notenoughcrashes.crashscreen.title": "Minecraftがクラッシュしました!",
"notenoughcrashes.crashscreen.summary": "Minecraftに問題が発生してクラッシュしました。",
"notenoughcrashes.crashscreen.paragraph1.line1": "以下のMODが潜在的な原因として特定されています:",
"notenoughcrashes.crashscreen.unknownCause": "不明",
"notenoughcrashes.crashscreen.identificationErrored": "[識別エラー、Not Enough Crashes に報告してください]",
"notenoughcrashes.crashscreen.noModsErrored": "特定されているModはありません。(Modが間接的にクラッシュを引き起こしている可能性があります。)",
"notenoughcrashes.crashscreen.paragraph2.line1": "リスト内のいずれかのMODをクリックすると、サポートのウェブサイトに移動します。",
"notenoughcrashes.crashscreen.paragraph2.line2": "レポートが作成されました。こちらからご覧いただけます(クリック):",
"notenoughcrashes.crashscreen.reportSaveFailed": "[レポートの保存中にエラーが発生しました。ログを参照してください。]",
"notenoughcrashes.crashscreen.paragraph3.line1": "「リンクを取得」ボタンをクリックしてブラウザで開きます。",
"notenoughcrashes.crashscreen.paragraph3.line2": "このレポートをModの作者に送って助けてもらうことをお勧めします",
"notenoughcrashes.crashscreen.paragraph3.line3": "問題を解決してください。Not Enough Crashesがインストールされているので、",
"notenoughcrashes.crashscreen.paragraph3.line4": "クラッシュにもかかわらずプレイを続けることができます。",
"notenoughcrashes.initerrorscreen.title": "Minecraftの起動に失敗しました!",
"notenoughcrashes.initerrorscreen.summary": "起動中にエラーが発生したため、Minecraftが起動できませんでした",
"notenoughcrashes.initerrorscreen.paragraph3.line1": "「リンクを取得」ボタンをクリックしてブラウザで開きます。",
"notenoughcrashes.initerrorscreen.paragraph3.line2": "このレポートをModの作者に送って助けてもらうことをお勧めします",
"notenoughcrashes.initerrorscreen.paragraph3.line3": "問題を解決してください。残念ながら、",
"notenoughcrashes.initerrorscreen.paragraph3.line4": "このエラーのためMinecraftをロードできません。",
"notenoughcrashes.midnightconfig.disableReturnToMainMenuComment1": "trueの場合、クラッシュ時に「メインメニューに戻る」ボタンが無効になります。",
"notenoughcrashes.midnightconfig.disableReturnToMainMenuComment2": "つまり、クラッシュから回復することはできません。",
"notenoughcrashes.midnightconfig.disableReturnToMainMenu": "メインメニューに戻る機能を無効にする",
"notenoughcrashes.midnightconfig.catchInitializationCrashesComment1": "(Fabricのみ) falseの場合、初期化に失敗するとゲームは通常通り終了します。",
"notenoughcrashes.midnightconfig.catchInitializationCrashesComment2": "特別なクラッシュ画面は表示されません。",
"notenoughcrashes.midnightconfig.catchInitializationCrashesComment3": "特定のModがインストールされている場合、これは常に無効になります(trueであっても)。",
"notenoughcrashes.midnightconfig.catchInitializationCrashes": "初期化クラッシュをキャッチする",
"notenoughcrashes.midnightconfig.debugModIdentificationComment": "trueの場合、Mod開発者用の追加情報が記録されます。",
"notenoughcrashes.midnightconfig.debugModIdentification": "デバッグModの識別",
"notenoughcrashes.midnightconfig.crashLimitComment": "NECが1回のセッションでゲームが終了するのを何回阻止しようとするか。",
"notenoughcrashes.midnightconfig.crashLimit": "クラッシュ限界",
"notenoughcrashes.midnightconfig.catchGameloopComment1": "falseの場合、NECはクラッシュ時にゲームが終了するのを防止しようとしません。",
"notenoughcrashes.midnightconfig.catchGameloopComment2": "Irisがインストールされている場合、これは常に無効になります(構成がtrueに設定されている場合でも)。",
"notenoughcrashes.midnightconfig.catchGameloop": "ゲームプレイのクラッシュをキャッチ",
"notenoughcrashes.midnightconfig.title": "Not Enough Crashes設定"
}
================================================
FILE: common/src/main/resources/assets/notenoughcrashes/lang/ms_my.json
================================================
{
"notenoughcrashes.gui.getLink": "Tunjukkan log ranap (tua)",
"notenoughcrashes.gui.uploadToCrashy": "Tunjukkan Log Ranap",
"notenoughcrashes.gui.loadingCrashyUpload": "Sedang memuat naik...",
"notenoughcrashes.gui.failed": "[Gagal, Laporkan kepada NEC]",
"notenoughcrashes.gui.keepPlaying": "Teruskan bermain",
"notenoughcrashes.gui.restart": "Mulakan Semula Minecraft",
"notenoughcrashes.gui.disabledByConfig": "Dinyahdayakan oleh konfigurasi",
"notenoughcrashes.crashscreen.title": "Minecraft telah ranap!",
"notenoughcrashes.crashscreen.summary": "Minecraft telah menghadapi masalah dan ranap.",
"notenoughcrashes.crashscreen.paragraph1.line1": "Mod berikut telah dikenal pasti sebagai punca yang berpotensi:",
"notenoughcrashes.crashscreen.unknownCause": "Tidak diketahui",
"notenoughcrashes.crashscreen.identificationErrored": "[Ralat dalam mengenal pasti, laporkan kepada Not Enough Crashes]",
"notenoughcrashes.crashscreen.noModsErrored": "Tiada Mod. (Mod masih boleh menyebabkan ranap permainan secara tidak langsung.)",
"notenoughcrashes.crashscreen.paragraph2.line1": "Klik mana-mana mod dalam senarai ini untuk pergi ke laman sesawang mereka untuk",
"notenoughcrashes.crashscreen.paragraph2.line2": "mendapatkan sokongan. Laporan telah dijana, dan boleh didapati di sini (klik):",
"notenoughcrashes.crashscreen.reportSaveFailed": "[Ralat menyimpan laporan, lihat log]",
"notenoughcrashes.crashscreen.paragraph3.line1": "Klik butang \"Dapatkan pautan\" untuk membukanya dalam penyemak imbas anda.",
"notenoughcrashes.crashscreen.paragraph3.line2": "Anda digalakkan untuk menghantar laporan ini kepada pengarang mod untuk membantu mereka",
"notenoughcrashes.crashscreen.paragraph3.line3": "menyelesaikan isu tersebut. Memandangkan Not Enough Crashes dipasang,",
"notenoughcrashes.crashscreen.paragraph3.line4": "anda boleh terus bermain walaupun setelah ranap berlaku.",
"notenoughcrashes.initerrorscreen.title": "Minecraft gagal dimulakan!",
"notenoughcrashes.initerrorscreen.summary": "Ralat semasa permulaan menghalang Minecraft daripada dimulakan",
"notenoughcrashes.initerrorscreen.paragraph3.line1": "Klik butang \"Dapatkan pautan\" untuk membukanya dalam penyemak imbas anda.",
"notenoughcrashes.initerrorscreen.paragraph3.line2": "Anda digalakkan untuk menghantar laporan ini kepada pengarang mod untuk membantu mereka",
"notenoughcrashes.initerrorscreen.paragraph3.line3": "menyelesaikan isu tersebut. Malangnya, ia adalah mustahil untuk terus",
"notenoughcrashes.initerrorscreen.paragraph3.line4": "memuatkan Minecraft kerana ralat ini.",
"notenoughcrashes.midnightconfig.disableReturnToMainMenuComment1": "Jika benar, butang \"Kembali ke Menu Utama\" akan dinyahdayakan apabila ranap, bermakna",
"notenoughcrashes.midnightconfig.disableReturnToMainMenuComment2": "anda tidak boleh memulihkan permainan anda daripada ranap.",
"notenoughcrashes.midnightconfig.disableReturnToMainMenu": "Nyahdayakan Kembali ke Skrin Utama",
"notenoughcrashes.midnightconfig.catchInitializationCrashesComment1": "(Fabric Sahaja) Jika palsu, permainan akan ditutup seperti biasa apabila ia gagal dimulakan,",
"notenoughcrashes.midnightconfig.catchInitializationCrashesComment2": "dan tidak akan memaparkan skrin ranap permainan khas.",
"notenoughcrashes.midnightconfig.catchInitializationCrashesComment3": "Ini akan sentiasa dinyahdayakan (walaupun benar) apabila mod tertentu dipasang.",
"notenoughcrashes.midnightconfig.catchInitializationCrashes": "Tangkap Ranap Pemulaan",
"notenoughcrashes.midnightconfig.debugModIdentificationComment": "Jika true, maklumat tambahan akan dilog untuk pembangun mod.",
"notenoughcrashes.midnightconfig.debugModIdentification": "Pengenalpastian Mod Nyahpepijat",
"notenoughcrashes.midnightconfig.crashLimitComment": "Berapa kali NEC akan cuba menghalang permainan daripada ditutup dalam satu sesi.",
"notenoughcrashes.midnightconfig.crashLimit": "Had Ranap",
"notenoughcrashes.midnightconfig.catchGameloopComment1": "Jika false, NEC tidak akan cuba menghalang permainan daripada ditutup apabila ranap.",
"notenoughcrashes.midnightconfig.catchGameloopComment2": "Ini akan sentiasa dinyahdayakan (walaupun konfigurasi ditetapkan kepada benar) apabila Iris dipasang.",
"notenoughcrashes.midnightconfig.catchGameloop": "Tangkap Ranap Semasa Bermain",
"notenoughcrashes.midnightconfig.title": "Konfigurasi Not Enough Crashes"
}
================================================
FILE: common/src/main/resources/assets/notenoughcrashes/lang/pt_br.json
================================================
{
"notenoughcrashes.gui.getLink": "Acessar link",
"notenoughcrashes.gui.failed": "[Falha]",
"notenoughcrashes.gui.keepPlaying": "Continuar jogando",
"notenoughcrashes.gui.restart": "Reiniciar o Minecraft",
"notenoughcrashes.gui.disabledByConfig": "Desativado pela config.",
"notenoughcrashes.crashscreen.title": "O Minecraft parou de funcionar!",
"notenoughcrashes.crashscreen.summary": "O Minecraft parou em decorrência de um erro.",
"notenoughcrashes.crashscreen.paragraph1.line1": "Os seguinte(s) mod(s) foram apontados como possíveis causas:",
"notenoughcrashes.crashscreen.unknownCause": "Desconhecido",
"notenoughcrashes.crashscreen.identificationErrored": "[Erro ao identificar, relatar ao Not Enough Crashes ]",
"notenoughcrashes.crashscreen.paragraph2.line1": "Clique em qualquer mod da lista para acessar a página de assistência. Um",
"notenoughcrashes.crashscreen.paragraph2.line2": "relatório foi criado e pode ser acessado aqui (clique):",
"notenoughcrashes.crashscreen.reportSaveFailed": "[Erro ao salvar o relatório, ver o registro]",
"notenoughcrashes.crashscreen.paragraph3.line1": "Clique em \"Acessar link\" para abri-lo em seu navegador. Se preferir,",
"notenoughcrashes.crashscreen.paragraph3.line2": "envie este relatório para o desenvolvedor do mod a fim de ajudá-lo",
"notenoughcrashes.crashscreen.paragraph3.line3": "a corrigir o erro. Já que o Not Enough Crashes está instalado, você pode",
"notenoughcrashes.crashscreen.paragraph3.line4": "continuar jogando.",
"notenoughcrashes.initerrorscreen.title": "O Minecraft não pôde ser inicializado!",
"notenoughcrashes.initerrorscreen.summary": "Um erro de inicialização impediu a execução do Minecraft",
"notenoughcrashes.initerrorscreen.paragraph3.line1": "Clique em \"Acessar link\" para abri-lo em seu navegador. Se preferir,",
"notenoughcrashes.initerrorscreen.paragraph3.line2": "envie este relatório ao desenvolvedor do mod para ajudá-lo a",
"notenoughcrashes.initerrorscreen.paragraph3.line3": "corrigir o erro. Lamentamos, não será possível continuar jogando",
"notenoughcrashes.initerrorscreen.paragraph3.line4": "o Minecraft devido ao erro."
}
================================================
FILE: common/src/main/resources/assets/notenoughcrashes/lang/ro_ro.json
================================================
{
"notenoughcrashes.gui.getLink": "Obțineți un link",
"notenoughcrashes.gui.failed": "[Eșuat]",
"notenoughcrashes.crashscreen.title": "Minecraft a crash!",
"notenoughcrashes.crashscreen.summary": "Minecraft întâmpinat o problemă și a crash",
"notenoughcrashes.crashscreen.paragraph1.line1": "Următoarele mod(uri) au fost identificate ca cauze posibile:",
"notenoughcrashes.crashscreen.paragraph2.line1": "Faceți click pe ori care mod din această lista pentru a merge",
"notenoughcrashes.crashscreen.paragraph2.line2": "pe site-ul lor pentru suport. Un raport poate fi găsit aici:",
"notenoughcrashes.crashscreen.reportSaveFailed": "[Eroare salvând raportul, vedeți log-ul]",
"notenoughcrashes.crashscreen.paragraph3.line1": "Faceți click pe butonul \"Obțineți un link\" pentru a-l deschide",
"notenoughcrashes.crashscreen.paragraph3.line2": "in browser. Sunteți encurajați să-l trimiteți la autorul modului",
"notenoughcrashes.crashscreen.paragraph3.line3": "pentru a-l ajuta să rezolve problema. Fiind că Not Enough Crashes e",
"notenoughcrashes.crashscreen.paragraph3.line4": "instalat, puteți continua de a juca în ciuda crash-ului."
}
================================================
FILE: common/src/main/resources/assets/notenoughcrashes/lang/ru_ru.json
================================================
{
"notenoughcrashes.gui.getLink": "Получить ссылку",
"notenoughcrashes.gui.failed": "[Ошибка]",
"notenoughcrashes.gui.keepPlaying": "Продолжить играть",
"notenoughcrashes.gui.restart": "Перезапустить Minecraft",
"notenoughcrashes.gui.disabledByConfig": "Отключено в настройках",
"notenoughcrashes.crashscreen.title": "Minecraft аварийно завершился!",
"notenoughcrashes.crashscreen.summary": "Minecraft столкнулся с проблемой и аварийно завершился.",
"notenoughcrashes.crashscreen.paragraph1.line1": "Следующие модификации были определены как возможные причины:",
"notenoughcrashes.crashscreen.unknownCause": "Неизвестно",
"notenoughcrashes.crashscreen.identificationErrored": "[Ошибка определения, сообщите разработчикам Not Enough Crashes]",
"notenoughcrashes.crashscreen.paragraph2.line1": "Нажмите на любую модификацию в списке, чтобы перейти на её сайт для получения",
"notenoughcrashes.crashscreen.paragraph2.line2": "помощи. Был сгенерирован отчёт о падении, он может быть найдет тут (нажмите):",
"notenoughcrashes.crashscreen.reportSaveFailed": "[Ошибка сохранения отчета, смотрите в журнале]",
"notenoughcrashes.crashscreen.paragraph3.line1": "Нажмите кнопку \"Получить ссылку\", чтобы открыть его в вашем браузере.",
"notenoughcrashes.crashscreen.paragraph3.line2": "Вы можете отправить этот отчёт автору модификации, чтобы помочь",
"notenoughcrashes.crashscreen.paragraph3.line3": "ему исправить проблему. Так как установлен Not Enough Crashes, вы можете",
"notenoughcrashes.crashscreen.paragraph3.line4": "продолжать играть несмотря на падение.",
"notenoughcrashes.initerrorscreen.title": "Minecraft не смог запуститься!",
"notenoughcrashes.initerrorscreen.summary": "Ошибка во время запуска помешала игре запуститься.",
"notenoughcrashes.initerrorscreen.paragraph3.line1": "Нажмите кнопку \"Получить ссылку\", чтобы открыть его в вашем браузере.",
"notenoughcrashes.initerrorscreen.paragraph3.line2": "Вы можете отправить этот отчёт автору модификации, чтобы помочь",
"notenoughcrashes.initerrorscreen.paragraph3.line3": "ему исправить проблему. К сожалению, из-за этой ошибки невозможно",
"notenoughcrashes.initerrorscreen.paragraph3.line4": "продолжить загрузку Minecraft."
}
================================================
FILE: common/src/main/resources/assets/notenoughcrashes/lang/tr_tr.json
================================================
{
"notenoughcrashes.gui.getLink": "Çökme günlüğünü göster",
"notenoughcrashes.gui.uploadToCrashy": "Crashy'ye yükle",
"notenoughcrashes.gui.loadingCrashyUpload": "Yükleniyor...",
"notenoughcrashes.gui.failed": "[Başarısız oldu, NEC'e bildir]",
"notenoughcrashes.gui.keepPlaying": "Oynamaya devam et",
"notenoughcrashes.gui.restart": "Minecraft'ı yeniden başlat",
"notenoughcrashes.gui.disabledByConfig": "Yapılandırma tarafından devre dışı bırakıldı",
"notenoughcrashes.crashscreen.title": "Minecraft çöktü!",
"notenoughcrashes.crashscreen.summary": "Minecraft bir sorunla karşılaştı ve çöktü.",
"notenoughcrashes.crashscreen.paragraph1.line1": "Şu mod(lar) potansiyel nedenler olarak belirlendi:",
"notenoughcrashes.crashscreen.unknownCause": "Bilinmeyen",
"notenoughcrashes.crashscreen.identificationErrored": "[Tanımlama hatası, Not Enough Crashes'e bildir]",
"notenoughcrashes.crashscreen.noModsErrored": "Mod yok. (Bir mod dolaylı olarak çöküşe neden olabilir.)",
"notenoughcrashes.crashscreen.paragraph2.line1": "Destek için listedeki herhangi bir moda tıklayarak web sitelerine gidin. Bir",
"notenoughcrashes.crashscreen.paragraph2.line2": "rapor oluşturuldu ve burada bulunabilir (tıklayın):",
"notenoughcrashes.crashscreen.reportSaveFailed": "[Rapor kaydedilirken hata oluştu, günlüğe bakın]",
"notenoughcrashes.crashscreen.paragraph3.line1": "Tarayıcınızda açmak için \"Bağlantıyı al\" düğmesine tıklayın. Bu raporu",
"notenoughcrashes.crashscreen.paragraph3.line2": "modun yapımcısına göndererek sorunu düzeltmelerine yardımcı olmanız",
"notenoughcrashes.crashscreen.paragraph3.line3": "önerilir. Not Enough Crashes yüklü olduğu için, çöküşe rağmen",
"notenoughcrashes.crashscreen.paragraph3.line4": "oynamaya devam edebilirsiniz.",
"notenoughcrashes.initerrorscreen.title": "Minecraft başlatılamadı!",
"notenoughcrashes.initerrorscreen.summary": "Başlangıç sırasında bir hata Minecraft'ın başlamasını engelledi",
"notenoughcrashes.initerrorscreen.paragraph3.line1": "Tarayıcınızda açmak için \"Bağlantıyı al\" düğmesine tıklayın. Bu raporu",
"notenoughcrashes.initerrorscreen.paragraph3.line2": "modun yapımcısına göndererek sorunu düzeltmelerine yardımcı olmanız",
"notenoughcrashes.initerrorscreen.paragraph3.line3": "önerilir. Maalesef, bu hata nedeniyle Minecraft'ı yüklemeye devam etmek",
"notenoughcrashes.initerrorscreen.paragraph3.line4": "mümkün değildir.",
"notenoughcrashes.midnightconfig.disableReturnToMainMenuComment1": "Doğruysa, çökme durumunda \"Ana Menüye Dön\" düğmesi devre dışı bırakılır,",
"notenoughcrashes.midnightconfig.disableReturnToMainMenuComment2": "yani bir çökmeden kurtulamazsınız.",
"notenoughcrashes.midnightconfig.disableReturnToMainMenu": "Ana Menüye Dön'ü Devre Dışı Bırak",
"notenoughcrashes.midnightconfig.catchInitializationCrashesComment1": "(Yalnızca Fabric) Yanlışsa, oyun başlatılamadığında normal şekilde kapanır",
"notenoughcrashes.midnightconfig.catchInitializationCrashesComment2": "ve özel bir çökme ekranı görüntülemez.",
"notenoughcrashes.midnightconfig.catchInitializationCrashesComment3": "Bu, belirli modlar yüklendiğinde her zaman devre dışı bırakılır (doğru olsa bile).",
"notenoughcrashes.midnightconfig.catchInitializationCrashes": "Başlatma Çökmelerini Yakala",
"notenoughcrashes.midnightconfig.debugModIdentificationComment": "Doğruysa, mod geliştiricisi için ek bilgiler günlüğe kaydedilir.",
"notenoughcrashes.midnightconfig.debugModIdentification": "Hata Ayıklama Modu Tanımlama",
"notenoughcrashes.midnightconfig.crashLimitComment": "NEC'in bir oturumda oyunun kapanmasını engellemeye kaç kez çalışacağı.",
"notenoughcrashes.midnightconfig.crashLimit": "Çökme Limiti",
"notenoughcrashes.midnightconfig.catchGameloopComment1": "Yanlışsa, NEC oyun çöktüğünde kapanmasını engellemeye çalışmaz.",
"notenoughcrashes.midnightconfig.catchGameloopComment2": "Bu, Iris
gitextract_0yxij5oi/ ├── .github/ │ └── ISSUE_TEMPLATE/ │ └── bug_report.md ├── .gitignore ├── Configuring Not Enough Crashes.md ├── LICENSE ├── README.md ├── TESTING.md ├── TestFabricMod/ │ ├── build.gradle │ └── src/ │ └── main/ │ ├── java/ │ │ └── io/ │ │ └── github/ │ │ └── natanfudge/ │ │ └── nectest/ │ │ ├── NecTestCrash.java │ │ ├── NecTestMod.java │ │ ├── NecTestModClient.java │ │ ├── TestSuppressedCloseable.java │ │ └── mixin/ │ │ ├── ExampleMixin.java │ │ └── MixinMinecraftServer.java │ └── resources/ │ ├── fabric.mod.json │ └── nec_testmod.mixins.json ├── TestForgeMod/ │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ └── main/ │ ├── java/ │ │ └── io/ │ │ └── github/ │ │ └── natanfudge/ │ │ └── nectest/ │ │ ├── NecTestCrash.java │ │ ├── NecTestMod.java │ │ ├── NecTestModClient.java │ │ └── mixin/ │ │ ├── ExampleMixin.java │ │ └── MixinMinecraftServer.java │ └── resources/ │ ├── META-INF/ │ │ └── mods.toml │ ├── nec_testmod.mixins.json │ └── pack.mcmeta ├── TooManyCrashes.license.md ├── build.gradle ├── changelog.md ├── common/ │ ├── build.gradle │ └── src/ │ └── main/ │ ├── java/ │ │ └── fudge/ │ │ └── notenoughcrashes/ │ │ ├── NotEnoughCrashes.java │ │ ├── StateManager.java │ │ ├── api/ │ │ │ └── NotEnoughCrashesApi.java │ │ ├── config/ │ │ │ ├── ButtonEntry.java │ │ │ ├── EntryInfo.java │ │ │ ├── MidnightConfig.java │ │ │ ├── MidnightConfigListWidget.java │ │ │ ├── MidnightConfigScreen.java │ │ │ ├── MidnightSliderWidget.java │ │ │ ├── NecConfig.java │ │ │ ├── NecMidnightConfig.java │ │ │ └── OldNecConfig.java │ │ ├── gui/ │ │ │ ├── CrashScreen.java │ │ │ ├── InitErrorScreen.java │ │ │ └── ProblemScreen.java │ │ ├── mixinhandlers/ │ │ │ ├── EntryPointCatcher.java │ │ │ └── InGameCatcher.java │ │ ├── mixins/ │ │ │ ├── MixinCrashReport.java │ │ │ ├── MixinTileEntity.java │ │ │ └── client/ │ │ │ ├── MixinKeyboard.java │ │ │ ├── MixinMinecraftClient.java │ │ │ ├── MixinMinecraftServer.java │ │ │ └── MixinMinecraftServerClientOnly.java │ │ ├── patches/ │ │ │ └── MinecraftClientAccess.java │ │ ├── platform/ │ │ │ ├── CommonModMetadata.java │ │ │ ├── ModsByLocation.java │ │ │ ├── NecPlatform.java │ │ │ └── NecPlatformStorage.java │ │ ├── stacktrace/ │ │ │ ├── CrashUtils.java │ │ │ └── ModIdentifier.java │ │ ├── upload/ │ │ │ ├── CrashyUpload.java │ │ │ ├── LegacyCrashLogUpload.java │ │ │ └── UploadToCrashyError.java │ │ └── utils/ │ │ ├── GlUtil.java │ │ ├── NecLocalization.java │ │ ├── SystemExitBlock.java │ │ └── SystemExitBlockedException.java │ └── resources/ │ ├── assets/ │ │ └── notenoughcrashes/ │ │ └── lang/ │ │ ├── de_de.json │ │ ├── en_us.json │ │ ├── et_ee.json │ │ ├── fr_fr.json │ │ ├── ja_jp.json │ │ ├── ms_my.json │ │ ├── pt_br.json │ │ ├── ro_ro.json │ │ ├── ru_ru.json │ │ ├── tr_tr.json │ │ ├── uk_ua.json │ │ ├── zh_cn.json │ │ ├── zh_hk.json │ │ ├── zh_tw.json │ │ └── zlm_arab.json │ └── notenoughcrashes.mixins.json ├── fabric/ │ ├── build.gradle │ └── src/ │ └── main/ │ ├── java/ │ │ └── fudge/ │ │ └── notenoughcrashes/ │ │ └── fabric/ │ │ ├── NotEnoughCrashesFabric.java │ │ ├── config/ │ │ │ └── ModMenuConfigIntegration.java │ │ ├── mixinhandlers/ │ │ │ └── ModLoaders.java │ │ ├── mixins/ │ │ │ ├── MixinMain.java │ │ │ └── client/ │ │ │ ├── CatchInitMinecraftClientMixin.java │ │ │ └── MixinMain.java │ │ └── platform/ │ │ └── FabricPlatform.java │ └── resources/ │ ├── fabric.mod.json │ ├── notenoughcrashes.accessWidener │ ├── notenoughcrashes.fabric.mixins.json │ └── quilt.mod.json ├── gradle/ │ ├── libs.versions.toml │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── neoforge/ │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ └── main/ │ ├── java/ │ │ └── fudge/ │ │ └── notenoughcrashes/ │ │ └── forge/ │ │ ├── NotEnoughCrashesForge.java │ │ ├── client/ │ │ │ └── NotEnoughCrashesForgeClient.java │ │ ├── mixins/ │ │ │ ├── MixinMain.java │ │ │ └── client/ │ │ │ └── MixinMain.java │ │ └── platform/ │ │ └── ForgePlatform.java │ └── resources/ │ ├── META-INF/ │ │ └── neoforge.mods.toml │ ├── notenoughcrashes.forge.mixins.json │ └── pack.mcmeta ├── pics/ │ └── logo 4.pdn └── settings.gradle
SYMBOL INDEX (301 symbols across 55 files)
FILE: TestFabricMod/src/main/java/io/github/natanfudge/nectest/NecTestCrash.java
class NecTestCrash (line 3) | public class NecTestCrash extends RuntimeException{
method NecTestCrash (line 4) | public NecTestCrash(String message){
FILE: TestFabricMod/src/main/java/io/github/natanfudge/nectest/NecTestMod.java
class NecTestMod (line 10) | public class NecTestMod implements ModInitializer {
method getTestMode (line 11) | public static String getTestMode() {
method onInitialize (line 26) | @Override
FILE: TestFabricMod/src/main/java/io/github/natanfudge/nectest/NecTestModClient.java
class NecTestModClient (line 10) | public class NecTestModClient implements ClientModInitializer {
method onInitializeClient (line 24) | @Override
FILE: TestFabricMod/src/main/java/io/github/natanfudge/nectest/TestSuppressedCloseable.java
class TestSuppressedCloseable (line 3) | public class TestSuppressedCloseable implements AutoCloseable {
method close (line 5) | @Override
FILE: TestFabricMod/src/main/java/io/github/natanfudge/nectest/mixin/ExampleMixin.java
class ExampleMixin (line 9) | @Mixin(TitleScreen.class)
method init (line 11) | @Inject(at = @At("HEAD"), method = "init()V")
FILE: TestFabricMod/src/main/java/io/github/natanfudge/nectest/mixin/MixinMinecraftServer.java
class MixinMinecraftServer (line 11) | @Mixin(MinecraftServer.class)
method testServerCrash (line 15) | @Inject(method = "runServer", at = @At(value = "INVOKE", target = "Lne...
FILE: TestForgeMod/src/main/java/io/github/natanfudge/nectest/NecTestCrash.java
class NecTestCrash (line 3) | public class NecTestCrash extends RuntimeException{
method NecTestCrash (line 4) | public NecTestCrash(String message){
FILE: TestForgeMod/src/main/java/io/github/natanfudge/nectest/NecTestMod.java
class NecTestMod (line 22) | @Mod("nec_testmod")
method getTestMode (line 24) | public static String getTestMode() {
method NecTestMod (line 42) | public NecTestMod() {
method setup (line 58) | private void setup(final FMLCommonSetupEvent event) {
class RegistryEvents (line 66) | @Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.FORGE)
method onClientTick (line 70) | @SubscribeEvent
FILE: TestForgeMod/src/main/java/io/github/natanfudge/nectest/NecTestModClient.java
class NecTestModClient (line 7) | public class NecTestModClient {
method onKeyRegister (line 10) | public static void onKeyRegister(RegisterKeyMappingsEvent event) {
FILE: TestForgeMod/src/main/java/io/github/natanfudge/nectest/mixin/ExampleMixin.java
class ExampleMixin (line 9) | @Mixin(TitleScreen.class)
method init (line 11) | @Inject(at = @At("HEAD"), method = "init()V")
FILE: TestForgeMod/src/main/java/io/github/natanfudge/nectest/mixin/MixinMinecraftServer.java
class MixinMinecraftServer (line 11) | @Mixin(MinecraftServer.class)
method testServerCrash (line 15) | @Inject(method = "runServer", at = @At(value = "INVOKE", target = "Lne...
FILE: common/src/main/java/fudge/notenoughcrashes/NotEnoughCrashes.java
class NotEnoughCrashes (line 16) | public class NotEnoughCrashes {
method getLogger (line 21) | public static Logger getLogger() {
method logDebug (line 28) | public static void logDebug(String message) {
method enableGameloopCatching (line 32) | public static boolean enableGameloopCatching() {
method enableEntrypointCatching (line 36) | public static boolean enableEntrypointCatching() {
method getMetadata (line 42) | public static CommonModMetadata getMetadata() {
method ensureDirectoryExists (line 48) | public static void ensureDirectoryExists() throws IOException {
method initialize (line 52) | public static void initialize() {
FILE: common/src/main/java/fudge/notenoughcrashes/config/ButtonEntry.java
class ButtonEntry (line 20) | public class ButtonEntry extends ElementListWidget.Entry<ButtonEntry> {
method ButtonEntry (line 28) | public ButtonEntry(List<ClickableWidget> buttons, Text text, EntryInfo...
method render (line 45) | public void render(DrawContext context, int mouseX, int mouseY, boolea...
method mouseClicked (line 64) | @Override
method children (line 71) | public List<? extends Element> children() {
method selectableChildren (line 75) | public List<? extends Selectable> selectableChildren() {
FILE: common/src/main/java/fudge/notenoughcrashes/config/EntryInfo.java
class EntryInfo (line 13) | public class EntryInfo {
method EntryInfo (line 29) | public EntryInfo(Field field, String modid) {
method setValue (line 50) | public void setValue(Object value) {
method toTemporaryValue (line 60) | public String toTemporaryValue() {
method updateFieldValue (line 69) | public void updateFieldValue() {
method updateConditions (line 77) | public void updateConditions() {
method writeList (line 92) | public <T> void writeList(int index, T value) {
method getTooltip (line 100) | public Tooltip getTooltip(boolean isButton) {
FILE: common/src/main/java/fudge/notenoughcrashes/config/MidnightConfig.java
class MidnightConfig (line 34) | @SuppressWarnings("unchecked")
method shouldSkipClass (line 42) | public boolean shouldSkipClass(Class<?> clazz) { return false; }
method shouldSkipField (line 43) | public boolean shouldSkipField(FieldAttributes fieldAttributes) { retu...
method write (line 46) | public void write(JsonWriter out, Identifier id) throws IOException { ...
method read (line 47) | public Identifier read(JsonReader in) throws IOException { return Iden...
method createInstance (line 58) | public static <T extends MidnightConfig> T createInstance(String modid...
method init (line 69) | public static void init(String modid, Class<? extends MidnightConfig> ...
method addClientEntry (line 83) | public void addClientEntry(Field field, EntryInfo info) {
method getUnderlyingType (line 111) | public static Class<?> getUnderlyingType(Field field) {
method textField (line 119) | private static void textField(EntryInfo info, Function<String,Number> ...
method getEnumTranslatableText (line 157) | protected Text getEnumTranslatableText(Object value, EntryInfo info) {
method loadValuesFromJson (line 164) | public void loadValuesFromJson() {
method write (line 182) | public static void write(String modid) {
method writeChanges (line 186) | @Deprecated
method writeChanges (line 191) | public void writeChanges() {
method getJsonFilePath (line 200) | public Path getJsonFilePath() {
method getDefaultValue (line 204) | @SuppressWarnings("unused") // Utility for mod authors
method onTabInit (line 211) | public void onTabInit(String tabName, MidnightConfigListWidget list, M...
method getScreen (line 214) | public static MidnightConfigScreen getScreen(Screen parent, String mod...
method getScreen (line 217) | public MidnightConfigScreen getScreen(Screen parent) {
FILE: common/src/main/java/fudge/notenoughcrashes/config/MidnightConfigListWidget.java
class MidnightConfigListWidget (line 13) | public class MidnightConfigListWidget extends ElementListWidget<ButtonEn...
method MidnightConfigListWidget (line 16) | public MidnightConfigListWidget(MinecraftClient client, int width, int...
method getScrollbarX (line 20) | @Override
method drawHeaderAndFooterSeparators (line 25) | @Override
method addButton (line 33) | public void addButton(List<ClickableWidget> buttons, Text text, EntryI...
method clear (line 37) | public void clear() {
method getRowWidth (line 41) | @Override
FILE: common/src/main/java/fudge/notenoughcrashes/config/MidnightConfigScreen.java
class MidnightConfigScreen (line 30) | public class MidnightConfigScreen extends Screen {
method MidnightConfigScreen (line 42) | public MidnightConfigScreen(Screen parent, String modid) {
method tick (line 68) | @Override
method updateButtons (line 86) | public void updateButtons() {
method keyPressed (line 100) | @Override
method close (line 105) | @Override
method init (line 120) | @Override
method updateList (line 144) | public void updateList() {
method render (line 255) | @Override
FILE: common/src/main/java/fudge/notenoughcrashes/config/MidnightSliderWidget.java
class MidnightSliderWidget (line 6) | public class MidnightSliderWidget extends SliderWidget {
method MidnightSliderWidget (line 10) | public MidnightSliderWidget(int x, int y, int width, int height, Text ...
method updateMessage (line 16) | @Override
method applyValue (line 21) | @Override
FILE: common/src/main/java/fudge/notenoughcrashes/config/NecConfig.java
method getCurrent (line 7) | public static NecConfig getCurrent() {
FILE: common/src/main/java/fudge/notenoughcrashes/config/NecMidnightConfig.java
class NecMidnightConfig (line 4) | public class NecMidnightConfig extends MidnightConfig {
FILE: common/src/main/java/fudge/notenoughcrashes/config/OldNecConfig.java
class OldNecConfig (line 11) | public class OldNecConfig {
class CrashUpload (line 24) | public static class CrashUpload {
type CrashLogUploadDestination (line 34) | public enum CrashLogUploadDestination {
method CrashLogUploadDestination (line 43) | CrashLogUploadDestination(@Nullable Integer defaultPriority) {
class Gist (line 48) | public static class Gist {
class Pastebin (line 53) | public static class Pastebin {
type Privacy (line 54) | public enum Privacy {
method Privacy (line 60) | Privacy(String apiValue) {
type Expiry (line 65) | public enum Expiry {
method Expiry (line 78) | Expiry(String pastebinExpiry) {
method instance (line 88) | public static OldNecConfig instance() {
FILE: common/src/main/java/fudge/notenoughcrashes/gui/CrashScreen.java
class CrashScreen (line 18) | @Environment(EnvType.CLIENT)
method CrashScreen (line 23) | public CrashScreen(CrashReport report) {
method init (line 28) | @Override
method centeredText (line 81) | private TextWidget centeredText(int centreX, int y, Text text, int col...
method centeredText (line 90) | private TextWidget centeredText(int centreX, int y, String translation...
method addBodyLine (line 98) | private int addBodyLine(int centreX, int currentY, int offset, String ...
method addBodyLine (line 106) | private int addBodyLine(int centreX, int currentY, int offset, String ...
FILE: common/src/main/java/fudge/notenoughcrashes/gui/InitErrorScreen.java
class InitErrorScreen (line 12) | @Environment(EnvType.CLIENT)
method InitErrorScreen (line 17) | public InitErrorScreen(CrashReport report) {
method init (line 24) | @Override
method centeredText (line 66) | private TextWidget centeredText(int centreX, int y, Text text, int col...
method centeredText (line 75) | private TextWidget centeredText(int centreX, int y, String keyOrLitera...
method addBodyLine (line 83) | private int addBodyLine(int centreX, int currentY, int offset, String ...
method addBodyLine (line 87) | private int addBodyLine(int centreX, int currentY, int offset, String ...
FILE: common/src/main/java/fudge/notenoughcrashes/gui/ProblemScreen.java
class ProblemScreen (line 26) | @Environment(EnvType.CLIENT)
method ProblemScreen (line 42) | protected ProblemScreen(CrashReport report) {
method getSuspectedModsText (line 48) | private Text getSuspectedModsText() {
method addSuspectedModsWidget (line 74) | private void addSuspectedModsWidget() {
method handleLegacyLinkClick (line 82) | private void handleLegacyLinkClick(ButtonWidget buttonWidget) {
method init (line 96) | @Override
method mouseClicked (line 113) | @Override
method shouldCloseOnEsc (line 124) | @Override
method getFileNameString (line 129) | String getFileNameString() {
method render (line 135) | @Override
FILE: common/src/main/java/fudge/notenoughcrashes/mixinhandlers/EntryPointCatcher.java
class EntryPointCatcher (line 15) | public class EntryPointCatcher {
method crashedDuringStartup (line 18) | public static boolean crashedDuringStartup() {
method handleEntryPointError (line 25) | @Environment(EnvType.CLIENT)
method displayInitErrorScreen (line 38) | @Environment(EnvType.CLIENT)
FILE: common/src/main/java/fudge/notenoughcrashes/mixinhandlers/InGameCatcher.java
class InGameCatcher (line 20) | public class InGameCatcher {
method handleClientCrash (line 27) | public static void handleClientCrash(CrashReport report) {
method resetStates (line 39) | private static void resetStates() {
method cleanupBeforeMinecraft (line 46) | public static void cleanupBeforeMinecraft() {
method resetCriticalGameState (line 58) | private static void resetCriticalGameState() {
method resetModState (line 72) | private static void resetModState() {
method handleServerCrash (line 78) | public static void handleServerCrash(CrashReport report) {
method getClient (line 84) | private static MinecraftClient getClient() {
method addInfoToCrash (line 88) | public static void addInfoToCrash(CrashReport report) {
method displayCrashScreen (line 93) | public static void displayCrashScreen(CrashReport report, int crashCou...
FILE: common/src/main/java/fudge/notenoughcrashes/mixins/MixinCrashReport.java
class MixinCrashReport (line 17) | @Mixin(value = CrashReport.class, priority = 500)
method getThis (line 24) | private CrashReport getThis() {
method beforeSystemDetailsAreWritten (line 31) | @Inject(method = "addDetails", at = @At(value = "INVOKE", target = "Ln...
FILE: common/src/main/java/fudge/notenoughcrashes/mixins/MixinTileEntity.java
class MixinTileEntity (line 10) | @Mixin(value = BlockEntity.class, priority = 10000)
method onPopulateCrashReport (line 15) | @SuppressWarnings("UnreachableCode")
FILE: common/src/main/java/fudge/notenoughcrashes/mixins/client/MixinKeyboard.java
class MixinKeyboard (line 10) | @Mixin(Keyboard.class)
method pollDebugCrashDontCrashInfinitely (line 16) | @Inject(method = "pollDebugCrash()V", at = @At("HEAD"), cancellable = ...
FILE: common/src/main/java/fudge/notenoughcrashes/mixins/client/MixinMinecraftClient.java
class MixinMinecraftClient (line 28) | @Mixin(MinecraftClient.class)
method printCrashReport (line 37) | @Shadow
method getRecorder (line 41) | @Override
method setRecorder (line 46) | @Override
method MixinMinecraftClient (line 51) | public MixinMinecraftClient(String string_1) {
method setScreenDontResetCrashScreen (line 59) | @Inject(method = "setScreen(Lnet/minecraft/client/gui/screen/Screen;)V...
method beforeRun (line 64) | @Inject(method = "run()V", at = @At("HEAD"))
method onCheckGameCrashed (line 74) | @Inject(method = "printCrashReport()V", at = @At("HEAD"))
method atTheEndOfFirstCatchBeforePrintingCrashReport (line 89) | @ModifyArg(method = "run()V", at = @At(value = "INVOKE", target = "Lne...
method atTheEndOfSecondCatchBeforePrintingCrashReport (line 100) | @ModifyArg(method = "run()V", at = @At(value = "INVOKE", target = "Lne...
method beforeCleanUpAfterCrash (line 116) | @Inject(method = "cleanUpAfterCrash()V", at = @At("HEAD"))
method redirectForgePrintCrashReport (line 138) | @SuppressWarnings({"MixinAnnotationTarget", "UnresolvedMixinReference"})
FILE: common/src/main/java/fudge/notenoughcrashes/mixins/client/MixinMinecraftServerClientOnly.java
class MixinMinecraftServerClientOnly (line 17) | @Mixin(MinecraftServer.class)
method disableIntegratedServerWriteToFileOnCrash (line 23) | @Redirect(method = "runServer()V", at = @At(value = "INVOKE", target =...
FILE: common/src/main/java/fudge/notenoughcrashes/patches/MinecraftClientAccess.java
type MinecraftClientAccess (line 5) | public interface MinecraftClientAccess {
method getRecorder (line 6) | Recorder getRecorder();
method setRecorder (line 7) | void setRecorder(Recorder recorder);
FILE: common/src/main/java/fudge/notenoughcrashes/platform/ModsByLocation.java
class ModsByLocation (line 14) | public class ModsByLocation {
method stringify (line 17) | private static String stringify(Set<CommonModMetadata> mods) {
method toString (line 21) | @Override
method ModsByLocation (line 28) | public ModsByLocation(Map<Path, Set<CommonModMetadata>> locationToMod) {
method get (line 36) | public Set<CommonModMetadata> get(URI path) {
method get (line 40) | @Nullable
method getOrEmpty (line 45) | @NotNull
method normalizePathString (line 52) | private static String normalizePathString(String path) {
method removeLastPercentSymbol (line 67) | private static String removeLastPercentSymbol(String str) {
method removeSuffix (line 79) | private static String removeSuffix(String str, String suffix) {
method removePrefix (line 83) | private static String removePrefix(String str, String suffix) {
method removeAndBefore (line 87) | private static String removeAndBefore(String str, String toRemove) {
method removeAndAfter (line 93) | private static String removeAndAfter(String str, String toRemove) {
FILE: common/src/main/java/fudge/notenoughcrashes/platform/NecPlatform.java
type NecPlatform (line 9) | public interface NecPlatform {
method instance (line 10) | static NecPlatform instance() {
method isForge (line 14) | boolean isForge();
method isModLoaded (line 16) | boolean isModLoaded(String modId);
method getModsAtLocationsInDisk (line 19) | ModsByLocation getModsAtLocationsInDisk();
method getGameDirectory (line 21) | Path getGameDirectory();
method getConfigDirectory (line 23) | Path getConfigDirectory();
method isDevelopmentEnvironment (line 25) | boolean isDevelopmentEnvironment();
method getResource (line 30) | @Nullable
method getModMetadatas (line 36) | List<CommonModMetadata> getModMetadatas(String modId);
method getAllMods (line 38) | List<CommonModMetadata> getAllMods();
method modContainsFile (line 40) | boolean modContainsFile(CommonModMetadata mod, String path);
method irisExists (line 42) | default boolean irisExists() {
method isClient (line 46) | boolean isClient();
method isClientEnv (line 48) | default boolean isClientEnv() {
FILE: common/src/main/java/fudge/notenoughcrashes/platform/NecPlatformStorage.java
class NecPlatformStorage (line 3) | public class NecPlatformStorage {
FILE: common/src/main/java/fudge/notenoughcrashes/stacktrace/CrashUtils.java
class CrashUtils (line 17) | public final class CrashUtils {
method outputClientReport (line 29) | public static void outputClientReport(CrashReport report) {
method outputReport (line 34) | public static void outputReport(CrashReport report, boolean isClient) {
FILE: common/src/main/java/fudge/notenoughcrashes/stacktrace/ModIdentifier.java
class ModIdentifier (line 27) | public final class ModIdentifier {
method getSuspectedModsOf (line 33) | public static Set<CommonModMetadata> getSuspectedModsOf(CrashReport re...
method identifyFromStacktrace (line 37) | @NotNull
method visitChildrenThrowables (line 51) | private static void visitChildrenThrowables(Throwable e, Consumer<Thro...
method identifyFromThrowable (line 56) | private static Set<CommonModMetadata> identifyFromThrowable(Throwable ...
method debug (line 85) | private static void debug(Supplier<String> message) {
method identifyFromClass (line 89) | @NotNull
method findMixinMerged (line 128) | @Nullable
method identifyFromMixin (line 149) | @NotNull
method getMixinInfo (line 163) | @Nullable
method getModsAt (line 176) | @NotNull
class Reflection (line 216) | private static class Reflection {
method getMixinInfo (line 228) | @Nullable
FILE: common/src/main/java/fudge/notenoughcrashes/upload/CrashyUpload.java
class CrashyUpload (line 20) | @SuppressWarnings("ConstantValue")
type CrashyMode (line 23) | private enum CrashyMode {
method uploadToCrashy (line 37) | public static CompletableFuture<String> uploadToCrashy(String text) th...
method uploadToCrashySync (line 64) | public static String uploadToCrashySync(String text) throws IOExceptio...
method java11PostAsync (line 68) | private static CompletableFuture<HttpResponse<String>> java11PostAsync...
method gzip (line 78) | private static byte[] gzip(String string) throws IOException {
method rememberCrashCode (line 87) | private static void rememberCrashCode(String id, String code) throws I...
class UploadCrashSuccess (line 98) | static class UploadCrashSuccess {
FILE: common/src/main/java/fudge/notenoughcrashes/upload/LegacyCrashLogUpload.java
class LegacyCrashLogUpload (line 18) | public final class LegacyCrashLogUpload {
method GIST_ACCESS_TOKEN_PART_1 (line 19) | private static String GIST_ACCESS_TOKEN_PART_1() {
method GIST_ACCESS_TOKEN_PART_2 (line 23) | private static String GIST_ACCESS_TOKEN_PART_2() {
class GistPost (line 31) | private static class GistPost {
method GistPost (line 36) | public GistPost(boolean isPublic, Map<String, GistFile> files) {
class GistFile (line 42) | private static class GistFile {
method GistFile (line 45) | public GistFile(String content) {
method upload (line 51) | public static String upload(String text) throws IOException {
method uploadToByteBin (line 163) | private static String uploadToByteBin(String text) throws IOException {
method createStringEntity (line 181) | private static StringEntity createStringEntity(String text) {
FILE: common/src/main/java/fudge/notenoughcrashes/upload/UploadToCrashyError.java
class UploadToCrashyError (line 3) | public abstract class UploadToCrashyError extends RuntimeException {
method UploadToCrashyError (line 4) | public UploadToCrashyError(String message) {
class InvalidCrash (line 8) | public static class InvalidCrash extends UploadToCrashyError {
method InvalidCrash (line 9) | public InvalidCrash() {
class TooLarge (line 14) | public static class TooLarge extends UploadToCrashyError {
method TooLarge (line 15) | public TooLarge() {
FILE: common/src/main/java/fudge/notenoughcrashes/utils/GlUtil.java
class GlUtil (line 11) | public class GlUtil {
method resetState (line 19) | public static void resetState() {
FILE: common/src/main/java/fudge/notenoughcrashes/utils/NecLocalization.java
class NecLocalization (line 26) | public class NecLocalization {
method localize (line 34) | public static String localize(String translationKey) {
method localizeCustom (line 39) | @NotNull
method localizeCustom (line 50) | @Nullable
method translatedText (line 58) | public static Text translatedText(String translationKey) {
class LanguageTranslations (line 63) | @SuppressWarnings("ClassCanBeRecord")
method LanguageTranslations (line 67) | private LanguageTranslations(Map<String, String> translations) {
method get (line 71) | @Nullable String get(String translationKey) {
method getCurrentLanguageCode (line 78) | private static String getCurrentLanguageCode() {
method loadLanguage (line 82) | private static LanguageTranslations loadLanguage(String code) {
method getLocalizations (line 99) | @Nullable
method parseTranslations (line 107) | private static Map<String, String> parseTranslations(String raw) {
FILE: common/src/main/java/fudge/notenoughcrashes/utils/SystemExitBlockedException.java
class SystemExitBlockedException (line 3) | class SystemExitBlockedException extends SecurityException {
method SystemExitBlockedException (line 4) | public SystemExitBlockedException(String s) {
FILE: fabric/src/main/java/fudge/notenoughcrashes/fabric/NotEnoughCrashesFabric.java
class NotEnoughCrashesFabric (line 8) | public class NotEnoughCrashesFabric implements ModInitializer {
method onInitialize (line 10) | @Override
FILE: fabric/src/main/java/fudge/notenoughcrashes/fabric/config/ModMenuConfigIntegration.java
class ModMenuConfigIntegration (line 8) | public class ModMenuConfigIntegration implements ModMenuApi {
method getModConfigScreenFactory (line 9) | @Override
FILE: fabric/src/main/java/fudge/notenoughcrashes/fabric/mixinhandlers/ModLoaders.java
class ModLoaders (line 7) | public class ModLoaders {
method fabricEntrypoints (line 8) | public static void fabricEntrypoints(File runDir, Object gameInstance) {
method quiltEntrypoints (line 11) | public static void quiltEntrypoints(File runDir, Object gameInstance) {
FILE: fabric/src/main/java/fudge/notenoughcrashes/fabric/mixins/MixinMain.java
class MixinMain (line 11) | @Mixin(Main.class)
method createPlatformInstanceAsSoonAsPossibleOnClient (line 13) | @Inject(method = "main", at = @At("HEAD"), remap = false)
FILE: fabric/src/main/java/fudge/notenoughcrashes/fabric/mixins/client/CatchInitMinecraftClientMixin.java
class CatchInitMinecraftClientMixin (line 13) | @Mixin(MinecraftClient.class)
method catchFabricInit (line 18) | @Redirect(method = "<init>", require = 0, at = @At(value = "INVOKE", t...
method catchQuiltInit (line 32) | @Redirect(method = "<init>", require = 0, at = @At(value = "INVOKE", t...
FILE: fabric/src/main/java/fudge/notenoughcrashes/fabric/mixins/client/MixinMain.java
class MixinMain (line 11) | @Mixin(Main.class)
method asSoonAsPossible (line 13) | @Inject(method = "main", at = @At("HEAD"), remap = false)
FILE: fabric/src/main/java/fudge/notenoughcrashes/fabric/platform/FabricPlatform.java
class FabricPlatform (line 24) | public class FabricPlatform implements NecPlatform {
method setIrisExists (line 27) | public void setIrisExists() {
method isForge (line 31) | @Override
method isModLoaded (line 36) | @Override
method getModsAtLocationsInDisk (line 41) | @Override
method getGameDirectory (line 52) | @Override
method getConfigDirectory (line 57) | @Override
method isDevelopmentEnvironment (line 62) | @Override
method getResourcesDirectory (line 67) | private Path getResourcesDirectory() {
method getResource (line 78) | @Override
method getModMetadatas (line 93) | @Override
method getAllMods (line 99) | @Override
method modContainsFile (line 106) | @Override
method irisExists (line 118) | @Override
method isClient (line 123) | @Override
method getIssuesPage (line 136) | private static String getIssuesPage(ContactInformation contactInformat...
method toCommon (line 144) | private static CommonModMetadata toCommon(ModContainer modContainer) {
FILE: neoforge/src/main/java/fudge/notenoughcrashes/forge/NotEnoughCrashesForge.java
class NotEnoughCrashesForge (line 9) | @Mod(NotEnoughCrashes.MOD_ID)
method NotEnoughCrashesForge (line 11) | public NotEnoughCrashesForge() {
FILE: neoforge/src/main/java/fudge/notenoughcrashes/forge/client/NotEnoughCrashesForgeClient.java
class NotEnoughCrashesForgeClient (line 11) | @Mod(value = NotEnoughCrashes.MOD_ID, dist = Dist.CLIENT)
method NotEnoughCrashesForgeClient (line 13) | public NotEnoughCrashesForgeClient(ModContainer container) {
FILE: neoforge/src/main/java/fudge/notenoughcrashes/forge/mixins/MixinMain.java
class MixinMain (line 11) | @Mixin(Main.class)
method createPlatformInstanceAsSoonAsPossibleOnClient (line 13) | @Inject(method = "main", at = @At("HEAD"), remap = false)
FILE: neoforge/src/main/java/fudge/notenoughcrashes/forge/mixins/client/MixinMain.java
class MixinMain (line 11) | @Mixin(Main.class)
method createPlatformInstanceAsSoonAsPossibleOnClient (line 13) | @Inject(method = "main", at = @At("HEAD"), remap = false)
FILE: neoforge/src/main/java/fudge/notenoughcrashes/forge/platform/ForgePlatform.java
class ForgePlatform (line 25) | public class ForgePlatform implements NecPlatform {
method isForge (line 26) | @Override
method isModLoaded (line 31) | @Override
method getModsAtLocationsInDisk (line 36) | @Override
method getGameDirectory (line 52) | @Override
method getConfigDirectory (line 57) | @Override
method isDevelopmentEnvironment (line 62) | @Override
method getResource (line 67) | @Override
method getModMetadatas (line 72) | @Override
method getAllMods (line 79) | @Override
method modContainsFile (line 86) | @Override
method isClient (line 102) | @Override
method toCommon (line 107) | private static CommonModMetadata toCommon(IModInfo imod) {
Condensed preview — 112 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (4,072K chars).
[
{
"path": ".github/ISSUE_TEMPLATE/bug_report.md",
"chars": 629,
"preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Describe the b"
},
{
"path": ".gitignore",
"chars": 196,
"preview": "build/\n*.ipr\nrun/\n*.iws\nout/\n*.iml\n.gradle/\noutput/\nbin/\nlibs/\n!run/mods/forge-template-loom.jar\n.classpath\n.project\n.id"
},
{
"path": "Configuring Not Enough Crashes.md",
"chars": 656,
"preview": "# Configuring Not Enough Crashes\n\n1. Open your minecraft instance at `C:\\Users\\<your-name>\\AppData\\Roaming\\.minecraft`\n\n"
},
{
"path": "LICENSE",
"chars": 1083,
"preview": "MIT License\n\nCopyright (c) 2021 Fudge and NEC contributors\n\nPermission is hereby granted, free of charge, to any person "
},
{
"path": "README.md",
"chars": 1372,
"preview": "# Not Enough Crashes\nDiscord: [](https://discord.gg/CF"
},
{
"path": "TESTING.md",
"chars": 3594,
"preview": "# Testing Not Enough Crashes\nThis documents details how to test Not Enough Crashes after making changes.\n\n- To test Fabr"
},
{
"path": "TestFabricMod/build.gradle",
"chars": 1636,
"preview": "import org.gradle.jvm.toolchain.JavaLanguageVersion\n\next.fabric_version = libs.versions.fabric.api.get()\n\narchivesBaseNa"
},
{
"path": "TestFabricMod/src/main/java/io/github/natanfudge/nectest/NecTestCrash.java",
"chars": 164,
"preview": "package io.github.natanfudge.nectest;\n\npublic class NecTestCrash extends RuntimeException{\n public NecTestCrash(Strin"
},
{
"path": "TestFabricMod/src/main/java/io/github/natanfudge/nectest/NecTestMod.java",
"chars": 1278,
"preview": "package io.github.natanfudge.nectest;\n\nimport net.fabricmc.api.ModInitializer;\nimport net.fabricmc.loader.api.FabricLoad"
},
{
"path": "TestFabricMod/src/main/java/io/github/natanfudge/nectest/NecTestModClient.java",
"chars": 1701,
"preview": "package io.github.natanfudge.nectest;\n\nimport net.fabricmc.api.ClientModInitializer;\nimport net.fabricmc.fabric.api.clie"
},
{
"path": "TestFabricMod/src/main/java/io/github/natanfudge/nectest/TestSuppressedCloseable.java",
"chars": 213,
"preview": "package io.github.natanfudge.nectest;\n\npublic class TestSuppressedCloseable implements AutoCloseable {\n\n @Override\n "
},
{
"path": "TestFabricMod/src/main/java/io/github/natanfudge/nectest/mixin/ExampleMixin.java",
"chars": 529,
"preview": "package io.github.natanfudge.nectest.mixin;\n\nimport net.minecraft.client.gui.screen.TitleScreen;\nimport org.spongepowere"
},
{
"path": "TestFabricMod/src/main/java/io/github/natanfudge/nectest/mixin/MixinMinecraftServer.java",
"chars": 873,
"preview": "package io.github.natanfudge.nectest.mixin;\n\nimport io.github.natanfudge.nectest.NecTestCrash;\nimport io.github.natanfud"
},
{
"path": "TestFabricMod/src/main/resources/fabric.mod.json",
"chars": 658,
"preview": "{\n \"schemaVersion\": 1,\n \"id\": \"nec_testmod\",\n \"version\": \"${version}\",\n\n \"name\": \"Not Enough Crashes Test Mod\",\n \"d"
},
{
"path": "TestFabricMod/src/main/resources/nec_testmod.mixins.json",
"chars": 262,
"preview": "{\n \"required\": true,\n \"minVersion\": \"0.8\",\n \"package\": \"io.github.natanfudge.nectest.mixin\",\n \"compatibilityLevel\": "
},
{
"path": "TestForgeMod/build.gradle",
"chars": 1120,
"preview": "plugins {\n alias libs.plugins.shadow\n}\n\narchivesBaseName = \"nec_testmod\"\nversion = \"1.0.0\"\ngroup = \"io.github.natanfu"
},
{
"path": "TestForgeMod/gradle.properties",
"chars": 76,
"preview": "# Necessary for Architectury Loom to build against Forge\nloom.platform=forge"
},
{
"path": "TestForgeMod/src/main/java/io/github/natanfudge/nectest/NecTestCrash.java",
"chars": 164,
"preview": "package io.github.natanfudge.nectest;\n\npublic class NecTestCrash extends RuntimeException{\n public NecTestCrash(Strin"
},
{
"path": "TestForgeMod/src/main/java/io/github/natanfudge/nectest/NecTestMod.java",
"chars": 2847,
"preview": "package io.github.natanfudge.nectest;\n\nimport cpw.mods.modlauncher.Launcher;\nimport net.minecraft.block.Blocks;\nimport n"
},
{
"path": "TestForgeMod/src/main/java/io/github/natanfudge/nectest/NecTestModClient.java",
"chars": 449,
"preview": "package io.github.natanfudge.nectest;\n\nimport net.minecraft.client.option.KeyBinding;\nimport net.minecraftforge.client.e"
},
{
"path": "TestForgeMod/src/main/java/io/github/natanfudge/nectest/mixin/ExampleMixin.java",
"chars": 529,
"preview": "package io.github.natanfudge.nectest.mixin;\n\nimport net.minecraft.client.gui.screen.TitleScreen;\nimport org.spongepowere"
},
{
"path": "TestForgeMod/src/main/java/io/github/natanfudge/nectest/mixin/MixinMinecraftServer.java",
"chars": 873,
"preview": "package io.github.natanfudge.nectest.mixin;\n\nimport io.github.natanfudge.nectest.NecTestCrash;\nimport io.github.natanfud"
},
{
"path": "TestForgeMod/src/main/resources/META-INF/mods.toml",
"chars": 4000,
"preview": "# This is an example mods.toml file. It contains the data relating to the loading mods.\n# There are several mandatory fi"
},
{
"path": "TestForgeMod/src/main/resources/nec_testmod.mixins.json",
"chars": 262,
"preview": "{\n \"required\": true,\n \"minVersion\": \"0.8\",\n \"package\": \"io.github.natanfudge.nectest.mixin\",\n \"compatibilityLevel\": "
},
{
"path": "TestForgeMod/src/main/resources/pack.mcmeta",
"chars": 96,
"preview": "{\n \"pack\": {\n \"description\": \"examplemod resources\",\n \"pack_format\": 7\n }\n}\n"
},
{
"path": "TooManyCrashes.license.md",
"chars": 1079,
"preview": "MIT License\n\nCopyright (c) 2018 Dimensional Development\n\nPermission is hereby granted, free of charge, to any person obt"
},
{
"path": "build.gradle",
"chars": 2375,
"preview": "plugins {\n alias libs.plugins.architectury.loom apply false\n alias libs.plugins.minotaur apply false\n alias lib"
},
{
"path": "changelog.md",
"chars": 8427,
"preview": "### 4.4.9\n- Added Japanese Translation\n### 4.4.8\n- Removed a fringe feature that was eating away at the computer's memor"
},
{
"path": "common/build.gradle",
"chars": 314,
"preview": "architectury {\n common(\"fabric\",\"neoforge\")\n}\n\ndependencies {\n // We depend on Fabric Loader here to use the Fabri"
},
{
"path": "common/src/main/java/fudge/notenoughcrashes/NotEnoughCrashes.java",
"chars": 2252,
"preview": "package fudge.notenoughcrashes;\n\nimport fudge.notenoughcrashes.config.MidnightConfig;\nimport fudge.notenoughcrashes.conf"
},
{
"path": "common/src/main/java/fudge/notenoughcrashes/StateManager.java",
"chars": 1123,
"preview": "package fudge.notenoughcrashes;\n\nimport java.lang.ref.WeakReference;\nimport java.util.HashSet;\nimport java.util.Iterator"
},
{
"path": "common/src/main/java/fudge/notenoughcrashes/api/NotEnoughCrashesApi.java",
"chars": 562,
"preview": "package fudge.notenoughcrashes.api;//package fudge.notenoughcrashes.api;\n//\n//import java.util.ArrayList;\n//import java."
},
{
"path": "common/src/main/java/fudge/notenoughcrashes/config/ButtonEntry.java",
"chars": 3466,
"preview": "package fudge.notenoughcrashes.config;\n\nimport com.google.common.collect.Lists;\nimport net.minecraft.client.MinecraftCli"
},
{
"path": "common/src/main/java/fudge/notenoughcrashes/config/EntryInfo.java",
"chars": 4195,
"preview": "package fudge.notenoughcrashes.config;\n\nimport fudge.notenoughcrashes.platform.NecPlatform;\nimport net.minecraft.client."
},
{
"path": "common/src/main/java/fudge/notenoughcrashes/config/MidnightConfig.java",
"chars": 16396,
"preview": "package fudge.notenoughcrashes.config;\n\nimport com.google.gson.*;\nimport com.google.gson.stream.*;\nimport fudge.notenoug"
},
{
"path": "common/src/main/java/fudge/notenoughcrashes/config/MidnightConfigListWidget.java",
"chars": 1494,
"preview": "package fudge.notenoughcrashes.config;\n\nimport net.minecraft.client.MinecraftClient;\nimport net.minecraft.client.gl.Rend"
},
{
"path": "common/src/main/java/fudge/notenoughcrashes/config/MidnightConfigScreen.java",
"chars": 13925,
"preview": "package fudge.notenoughcrashes.config;\n\nimport com.google.common.collect.Lists;\nimport net.minecraft.client.gui.DrawCont"
},
{
"path": "common/src/main/java/fudge/notenoughcrashes/config/MidnightSliderWidget.java",
"chars": 1111,
"preview": "package fudge.notenoughcrashes.config;\n\nimport net.minecraft.client.gui.widget.SliderWidget;\nimport net.minecraft.text.T"
},
{
"path": "common/src/main/java/fudge/notenoughcrashes/config/NecConfig.java",
"chars": 529,
"preview": "package fudge.notenoughcrashes.config;\n\n\npublic record NecConfig(boolean disableReturnToMainMenu, boolean catchInitializ"
},
{
"path": "common/src/main/java/fudge/notenoughcrashes/config/NecMidnightConfig.java",
"chars": 1044,
"preview": "package fudge.notenoughcrashes.config;\n\n\npublic class NecMidnightConfig extends MidnightConfig {\n @Comment\n public"
},
{
"path": "common/src/main/java/fudge/notenoughcrashes/config/OldNecConfig.java",
"chars": 3648,
"preview": "package fudge.notenoughcrashes.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport fudge.no"
},
{
"path": "common/src/main/java/fudge/notenoughcrashes/gui/CrashScreen.java",
"chars": 4717,
"preview": "package fudge.notenoughcrashes.gui;\n\nimport fudge.notenoughcrashes.config.NecConfig;\nimport fudge.notenoughcrashes.mixin"
},
{
"path": "common/src/main/java/fudge/notenoughcrashes/gui/InitErrorScreen.java",
"chars": 4113,
"preview": "package fudge.notenoughcrashes.gui;\n\nimport fudge.notenoughcrashes.utils.NecLocalization;\nimport net.fabricmc.api.EnvTyp"
},
{
"path": "common/src/main/java/fudge/notenoughcrashes/gui/ProblemScreen.java",
"chars": 5003,
"preview": "package fudge.notenoughcrashes.gui;\n\nimport fudge.notenoughcrashes.NotEnoughCrashes;\nimport fudge.notenoughcrashes.platf"
},
{
"path": "common/src/main/java/fudge/notenoughcrashes/mixinhandlers/EntryPointCatcher.java",
"chars": 1918,
"preview": "package fudge.notenoughcrashes.mixinhandlers;\n\nimport fudge.notenoughcrashes.NotEnoughCrashes;\nimport fudge.notenoughcra"
},
{
"path": "common/src/main/java/fudge/notenoughcrashes/mixinhandlers/InGameCatcher.java",
"chars": 4961,
"preview": "package fudge.notenoughcrashes.mixinhandlers;\n\nimport fudge.notenoughcrashes.NotEnoughCrashes;\nimport fudge.notenoughcra"
},
{
"path": "common/src/main/java/fudge/notenoughcrashes/mixins/MixinCrashReport.java",
"chars": 1727,
"preview": "package fudge.notenoughcrashes.mixins;\n\nimport fudge.notenoughcrashes.stacktrace.ModIdentifier;\nimport net.minecraft.uti"
},
{
"path": "common/src/main/java/fudge/notenoughcrashes/mixins/MixinTileEntity.java",
"chars": 1004,
"preview": "package fudge.notenoughcrashes.mixins;\n\nimport net.minecraft.block.entity.BlockEntity;\nimport net.minecraft.util.crash.C"
},
{
"path": "common/src/main/java/fudge/notenoughcrashes/mixins/client/MixinKeyboard.java",
"chars": 824,
"preview": "package fudge.notenoughcrashes.mixins.client;\n\nimport fudge.notenoughcrashes.mixinhandlers.InGameCatcher;\nimport net.min"
},
{
"path": "common/src/main/java/fudge/notenoughcrashes/mixins/client/MixinMinecraftClient.java",
"chars": 6837,
"preview": "package fudge.notenoughcrashes.mixins.client;\n\nimport fudge.notenoughcrashes.NotEnoughCrashes;\nimport fudge.notenoughcra"
},
{
"path": "common/src/main/java/fudge/notenoughcrashes/mixins/client/MixinMinecraftServer.java",
"chars": 405,
"preview": "package fudge.notenoughcrashes.mixins.client;\n\n//\n///**\n// * Mixin to stop the integrated server from ticking when the g"
},
{
"path": "common/src/main/java/fudge/notenoughcrashes/mixins/client/MixinMinecraftServerClientOnly.java",
"chars": 1261,
"preview": "package fudge.notenoughcrashes.mixins.client;\n\nimport fudge.notenoughcrashes.NotEnoughCrashes;\nimport net.minecraft.serv"
},
{
"path": "common/src/main/java/fudge/notenoughcrashes/patches/MinecraftClientAccess.java",
"chars": 201,
"preview": "package fudge.notenoughcrashes.patches;\n\nimport net.minecraft.util.profiler.Recorder;\n\npublic interface MinecraftClientA"
},
{
"path": "common/src/main/java/fudge/notenoughcrashes/platform/CommonModMetadata.java",
"chars": 444,
"preview": "package fudge.notenoughcrashes.platform;\n\nimport org.jetbrains.annotations.Nullable;\n\nimport java.nio.file.Path;\nimport "
},
{
"path": "common/src/main/java/fudge/notenoughcrashes/platform/ModsByLocation.java",
"chars": 3770,
"preview": "package fudge.notenoughcrashes.platform;\n\nimport org.jetbrains.annotations.NotNull;\nimport org.jetbrains.annotations.Nul"
},
{
"path": "common/src/main/java/fudge/notenoughcrashes/platform/NecPlatform.java",
"chars": 1129,
"preview": "package fudge.notenoughcrashes.platform;\n\nimport org.jetbrains.annotations.Nullable;\n\nimport java.io.InputStream;\nimport"
},
{
"path": "common/src/main/java/fudge/notenoughcrashes/platform/NecPlatformStorage.java",
"chars": 167,
"preview": "package fudge.notenoughcrashes.platform;\n\npublic class NecPlatformStorage {\n public static NecPlatform INSTANCE_SET_O"
},
{
"path": "common/src/main/java/fudge/notenoughcrashes/stacktrace/CrashUtils.java",
"chars": 2065,
"preview": "package fudge.notenoughcrashes.stacktrace;\n\nimport fudge.notenoughcrashes.NotEnoughCrashes;\nimport fudge.notenoughcrashe"
},
{
"path": "common/src/main/java/fudge/notenoughcrashes/stacktrace/ModIdentifier.java",
"chars": 10518,
"preview": "package fudge.notenoughcrashes.stacktrace;\n\nimport fudge.notenoughcrashes.NotEnoughCrashes;\nimport fudge.notenoughcrashe"
},
{
"path": "common/src/main/java/fudge/notenoughcrashes/upload/CrashyUpload.java",
"chars": 4311,
"preview": "package fudge.notenoughcrashes.upload;\n\nimport com.google.gson.Gson;\nimport fudge.notenoughcrashes.NotEnoughCrashes;\n\nim"
},
{
"path": "common/src/main/java/fudge/notenoughcrashes/upload/LegacyCrashLogUpload.java",
"chars": 8733,
"preview": "package fudge.notenoughcrashes.upload;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonObject;\nimport com.googl"
},
{
"path": "common/src/main/java/fudge/notenoughcrashes/upload/UploadToCrashyError.java",
"chars": 522,
"preview": "package fudge.notenoughcrashes.upload;\n\npublic abstract class UploadToCrashyError extends RuntimeException {\n public "
},
{
"path": "common/src/main/java/fudge/notenoughcrashes/utils/GlUtil.java",
"chars": 3233,
"preview": "package fudge.notenoughcrashes.utils;\n\nimport com.mojang.blaze3d.opengl.GlStateManager;\nimport com.mojang.blaze3d.system"
},
{
"path": "common/src/main/java/fudge/notenoughcrashes/utils/NecLocalization.java",
"chars": 4729,
"preview": "package fudge.notenoughcrashes.utils;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonObject;\nimport fudge.note"
},
{
"path": "common/src/main/java/fudge/notenoughcrashes/utils/SystemExitBlock.java",
"chars": 908,
"preview": "package fudge.notenoughcrashes.utils;//package fudge.notenoughcrashes.utils;\n//\n//import fudge.notenoughcrashes.config.N"
},
{
"path": "common/src/main/java/fudge/notenoughcrashes/utils/SystemExitBlockedException.java",
"chars": 177,
"preview": "package fudge.notenoughcrashes.utils;\n\nclass SystemExitBlockedException extends SecurityException {\n public SystemExi"
},
{
"path": "common/src/main/resources/assets/notenoughcrashes/lang/de_de.json",
"chars": 1232,
"preview": "{\n \"notenoughcrashes.gui.getLink\": \"Link erhalten\",\n \"notenoughcrashes.gui.failed\": \"[Fehlgeschlagen]\",\n \"notenoughcr"
},
{
"path": "common/src/main/resources/assets/notenoughcrashes/lang/en_us.json",
"chars": 4096,
"preview": "{\n \"notenoughcrashes.gui.getLink\": \"Show crash log\",\n \"notenoughcrashes.gui.uploadToCrashy\": \"Upload to Crashy\",\n \"no"
},
{
"path": "common/src/main/resources/assets/notenoughcrashes/lang/et_ee.json",
"chars": 2250,
"preview": "{\n \"notenoughcrashes.gui.getLink\": \"Hangi link\",\n \"notenoughcrashes.gui.failed\": \"[Ebaõnnestus]\",\n \"notenoughcrashes."
},
{
"path": "common/src/main/resources/assets/notenoughcrashes/lang/fr_fr.json",
"chars": 1421,
"preview": "{\n \"notenoughcrashes.gui.getLink\": \"Obtenir un lien\",\n \"notenoughcrashes.gui.failed\": \"[Échoué]\",\n \"notenoughcrashes."
},
{
"path": "common/src/main/resources/assets/notenoughcrashes/lang/ja_jp.json",
"chars": 3363,
"preview": "{\n \"notenoughcrashes.gui.getLink\": \"クラッシュログを表示\",\n \"notenoughcrashes.gui.uploadToCrashy\": \"Crashyにアップロード\",\n \"notenough"
},
{
"path": "common/src/main/resources/assets/notenoughcrashes/lang/ms_my.json",
"chars": 4441,
"preview": "{\n \"notenoughcrashes.gui.getLink\": \"Tunjukkan log ranap (tua)\",\n \"notenoughcrashes.gui.uploadToCrashy\": \"Tunjukkan Log"
},
{
"path": "common/src/main/resources/assets/notenoughcrashes/lang/pt_br.json",
"chars": 2172,
"preview": "{\n \"notenoughcrashes.gui.getLink\": \"Acessar link\",\n \"notenoughcrashes.gui.failed\": \"[Falha]\",\n \"notenoughcrashes.gui."
},
{
"path": "common/src/main/resources/assets/notenoughcrashes/lang/ro_ro.json",
"chars": 1163,
"preview": "{\n \"notenoughcrashes.gui.getLink\": \"Obțineți un link\",\n \"notenoughcrashes.gui.failed\": \"[Eșuat]\",\n \"notenoughcrashes."
},
{
"path": "common/src/main/resources/assets/notenoughcrashes/lang/ru_ru.json",
"chars": 2236,
"preview": "{\n \"notenoughcrashes.gui.getLink\": \"Получить ссылку\",\n \"notenoughcrashes.gui.failed\": \"[Ошибка]\",\n \"notenoughcrashes."
},
{
"path": "common/src/main/resources/assets/notenoughcrashes/lang/tr_tr.json",
"chars": 4170,
"preview": "{\n \"notenoughcrashes.gui.getLink\": \"Çökme günlüğünü göster\",\n \"notenoughcrashes.gui.uploadToCrashy\": \"Crashy'ye yükle\""
},
{
"path": "common/src/main/resources/assets/notenoughcrashes/lang/uk_ua.json",
"chars": 2240,
"preview": "{\n \"notenoughcrashes.gui.getLink\": \"Отримати посилання\",\n \"notenoughcrashes.gui.failed\": \"[Помилка]\",\n \"notenoughcras"
},
{
"path": "common/src/main/resources/assets/notenoughcrashes/lang/zh_cn.json",
"chars": 1704,
"preview": "{\n \"notenoughcrashes.gui.getLink\": \"获取链接\",\n \"notenoughcrashes.gui.uploadToCrashy\": \"在Crashy上显示\",\n \"notenoughcrashes.g"
},
{
"path": "common/src/main/resources/assets/notenoughcrashes/lang/zh_hk.json",
"chars": 1717,
"preview": "{\n \"notenoughcrashes.gui.getLink\": \"獲取連結\",\n \"notenoughcrashes.gui.uploadToCrashy\": \"在 Crashy上 顯示\",\n \"notenoughcrashes"
},
{
"path": "common/src/main/resources/assets/notenoughcrashes/lang/zh_tw.json",
"chars": 3114,
"preview": "{\n \"notenoughcrashes.gui.getLink\": \"獲取連結\",\n \"notenoughcrashes.gui.uploadToCrashy\": \"在 Crashy 上顯示\",\n \"notenoughc"
},
{
"path": "common/src/main/resources/assets/notenoughcrashes/lang/zlm_arab.json",
"chars": 4094,
"preview": "{\n \"notenoughcrashes.gui.getLink\": \"تونجوقکن لوݢ رانڤ (توا)\",\n \"notenoughcrashes.gui.uploadToCrashy\": \"تونجوقکن لوݢ را"
},
{
"path": "common/src/main/resources/notenoughcrashes.mixins.json",
"chars": 347,
"preview": "{\n \"package\": \"fudge.notenoughcrashes.mixins\",\n \"required\": true,\n \"target\": \"@env(DEFAULT)\",\n \"compatibilityLevel\":"
},
{
"path": "fabric/build.gradle",
"chars": 2778,
"preview": "plugins {\n alias libs.plugins.shadow\n}\n\narchitectury {\n platformSetupLoomIde()\n fabric()\n}\n\nconfigurations {\n "
},
{
"path": "fabric/src/main/java/fudge/notenoughcrashes/fabric/NotEnoughCrashesFabric.java",
"chars": 399,
"preview": "package fudge.notenoughcrashes.fabric;\n\nimport fudge.notenoughcrashes.NotEnoughCrashes;\nimport fudge.notenoughcrashes.co"
},
{
"path": "fabric/src/main/java/fudge/notenoughcrashes/fabric/config/ModMenuConfigIntegration.java",
"chars": 491,
"preview": "package fudge.notenoughcrashes.fabric.config;\n\nimport com.terraformersmc.modmenu.api.ConfigScreenFactory;\nimport com.ter"
},
{
"path": "fabric/src/main/java/fudge/notenoughcrashes/fabric/mixinhandlers/ModLoaders.java",
"chars": 461,
"preview": "package fudge.notenoughcrashes.fabric.mixinhandlers;\n\nimport net.fabricmc.loader.impl.game.minecraft.Hooks;\n\nimport java"
},
{
"path": "fabric/src/main/java/fudge/notenoughcrashes/fabric/mixins/MixinMain.java",
"chars": 735,
"preview": "package fudge.notenoughcrashes.fabric.mixins;\n\nimport fudge.notenoughcrashes.platform.NecPlatformStorage;\nimport fudge.n"
},
{
"path": "fabric/src/main/java/fudge/notenoughcrashes/fabric/mixins/client/CatchInitMinecraftClientMixin.java",
"chars": 1989,
"preview": "package fudge.notenoughcrashes.fabric.mixins.client;\n\nimport fudge.notenoughcrashes.NotEnoughCrashes;\nimport fudge.noten"
},
{
"path": "fabric/src/main/java/fudge/notenoughcrashes/fabric/mixins/client/MixinMain.java",
"chars": 717,
"preview": "package fudge.notenoughcrashes.fabric.mixins.client;\n\nimport fudge.notenoughcrashes.platform.NecPlatformStorage;\nimport "
},
{
"path": "fabric/src/main/java/fudge/notenoughcrashes/fabric/platform/FabricPlatform.java",
"chars": 5317,
"preview": "package fudge.notenoughcrashes.fabric.platform;\n\nimport fudge.notenoughcrashes.NotEnoughCrashes;\nimport fudge.notenoughc"
},
{
"path": "fabric/src/main/resources/fabric.mod.json",
"chars": 1065,
"preview": "{\n \"schemaVersion\": 1,\n \"id\": \"notenoughcrashes\",\n \"name\": \"Not Enough Crashes\",\n \"description\": \"Improves crashes i"
},
{
"path": "fabric/src/main/resources/notenoughcrashes.accessWidener",
"chars": 295,
"preview": "accessWidener v1 named\naccessible class net/minecraft/client/gui/screen/SplashScreen$LogoTexture\naccessible method"
},
{
"path": "fabric/src/main/resources/notenoughcrashes.fabric.mixins.json",
"chars": 284,
"preview": "{\n \"package\": \"fudge.notenoughcrashes.fabric.mixins\",\n \"required\": true,\n \"target\": \"@env(DEFAULT)\",\n \"minVersion\": "
},
{
"path": "fabric/src/main/resources/quilt.mod.json",
"chars": 1403,
"preview": "{\n \"schema_version\": 1,\n \"quilt_loader\": {\n \"group\": \"fudge.notenoughcrashes\",\n \"id\": \"notenoughcrashes\",\n \"v"
},
{
"path": "gradle/libs.versions.toml",
"chars": 1785,
"preview": "## Generated by $ ./gradlew refreshVersionsCatalog\n\n[plugins]\n\narchitectury_plugin = { id = \"architectury-plugin\", versi"
},
{
"path": "gradle/wrapper/gradle-wrapper.properties",
"chars": 423,
"preview": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\n# Upgrading to gradle 8.12 causes the fabric/forge jars"
},
{
"path": "gradle.properties",
"chars": 337,
"preview": "\norg.gradle.parallel=true\norg.gradle.jvmargs=-Xmx4048M\n# Not supported by architectury, see https://github.com/architect"
},
{
"path": "gradlew",
"chars": 8669,
"preview": "#!/bin/sh\n\n#\n# Copyright © 2015-2021 the original authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"Lice"
},
{
"path": "gradlew.bat",
"chars": 2826,
"preview": "@rem\n@rem Copyright 2015 the original author or authors.\n@rem\n@rem Licensed under the Apache License, Version 2.0 (the \""
},
{
"path": "neoforge/build.gradle",
"chars": 2445,
"preview": "plugins {\n alias libs.plugins.shadow\n}\n\narchitectury {\n platformSetupLoomIde()\n neoForge()\n}\n\nconfigurations {\n"
},
{
"path": "neoforge/gradle.properties",
"chars": 25,
"preview": "loom.platform = neoforge\n"
},
{
"path": "neoforge/src/main/java/fudge/notenoughcrashes/forge/NotEnoughCrashesForge.java",
"chars": 374,
"preview": "package fudge.notenoughcrashes.forge;\n\nimport fudge.notenoughcrashes.NotEnoughCrashes;\nimport fudge.notenoughcrashes.con"
},
{
"path": "neoforge/src/main/java/fudge/notenoughcrashes/forge/client/NotEnoughCrashesForgeClient.java",
"chars": 757,
"preview": "package fudge.notenoughcrashes.forge.client;\n\nimport fudge.notenoughcrashes.NotEnoughCrashes;\nimport fudge.notenoughcras"
},
{
"path": "neoforge/src/main/java/fudge/notenoughcrashes/forge/mixins/MixinMain.java",
"chars": 732,
"preview": "package fudge.notenoughcrashes.forge.mixins;\n\nimport fudge.notenoughcrashes.forge.platform.ForgePlatform;\nimport fudge.n"
},
{
"path": "neoforge/src/main/java/fudge/notenoughcrashes/forge/mixins/client/MixinMain.java",
"chars": 744,
"preview": "package fudge.notenoughcrashes.forge.mixins.client;\n\nimport fudge.notenoughcrashes.forge.platform.ForgePlatform;\nimport "
},
{
"path": "neoforge/src/main/java/fudge/notenoughcrashes/forge/platform/ForgePlatform.java",
"chars": 3999,
"preview": "package fudge.notenoughcrashes.forge.platform;\n\nimport fudge.notenoughcrashes.NotEnoughCrashes;\nimport fudge.notenoughcr"
},
{
"path": "neoforge/src/main/resources/META-INF/neoforge.mods.toml",
"chars": 989,
"preview": "modLoader = \"javafml\"\nloaderVersion = \"[3,)\"\nissueTrackerURL = \"https://github.com/natanfudge/Not-Enough-Crashes/issues\""
},
{
"path": "neoforge/src/main/resources/notenoughcrashes.forge.mixins.json",
"chars": 239,
"preview": "{\n \"package\": \"fudge.notenoughcrashes.forge.mixins\",\n \"required\": true,\n \"target\": \"@env(DEFAULT)\",\n \"compatibilityL"
},
{
"path": "neoforge/src/main/resources/pack.mcmeta",
"chars": 82,
"preview": "{\n \"pack\": {\n \"description\": \"Not Enough Crashes\",\n \"pack_format\": 6\n }\n}\n"
},
{
"path": "pics/logo 4.pdn",
"chars": 1834910,
"preview": "PDN3P\u0002<pdnImage width=\"1024\" height=\"1024\" layers=\"3\" savedWithVersion=\"4.312.8267.29064\"><custom><thumb png=\"iVBORw0KGg"
},
{
"path": "settings.gradle",
"chars": 390,
"preview": "pluginManagement {\n repositories {\n maven { url \"https://maven.fabricmc.net/\" }\n maven { url \"https://m"
}
]
// ... and 1 more files (download for full content)
About this extraction
This page contains the full source code of the natanfudge/Not-Enough-Crashes GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 112 files (2.0 MB), approximately 1.7M tokens, and a symbol index with 301 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.