commit 4c52efa3df5d8dbae6a4ebcf14d723b356d57003 Author: deechael Date: Sat Oct 5 14:59:54 2024 +0800 init diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..20fc528 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,15 @@ +* text eol=lf +*.bat text eol=crlf +*.patch text eol=lf +*.java text eol=lf +*.gradle text eol=crlf +*.png binary +*.gif binary +*.exe binary +*.dll binary +*.jar binary +*.lzma binary +*.zip binary +*.pyd binary +*.cfg text eol=lf +*.jks binary \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..45937e8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# eclipse +bin/ +*.launch +.settings +.metadata +.classpath +.project + +# idea +out/ +*.ipr +*.iws +*.iml +.idea/* +!.idea/scopes + +# gradle +build/ +.gradle/ + +# other +eclipse/ +run/ +runs/ + +# deechael +.temp/ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0e259d4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c61251d --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +# Concentration +Add borderless support for minecraft. +### A method to test if the borderless works +Try use [snipaste](https://www.snipaste.com) to take screenshot, if the screenshot shows exactly what you see in the game, means borderless works. If the screenshot taken is a previous-seen picture, means borderless not works. \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..5b7a2c5 --- /dev/null +++ b/build.gradle @@ -0,0 +1,6 @@ +plugins { + // see https://fabricmc.net/develop/ for new versions + id 'fabric-loom' version '1.7-SNAPSHOT' apply false + // see https://projects.neoforged.net/neoforged/moddevgradle for new versions + id 'net.neoforged.moddev' version '0.1.110' apply false +} \ No newline at end of file diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle new file mode 100644 index 0000000..6784052 --- /dev/null +++ b/buildSrc/build.gradle @@ -0,0 +1,3 @@ +plugins { + id 'groovy-gradle-plugin' +} diff --git a/buildSrc/src/main/groovy/multiloader-common.gradle b/buildSrc/src/main/groovy/multiloader-common.gradle new file mode 100644 index 0000000..e2b0047 --- /dev/null +++ b/buildSrc/src/main/groovy/multiloader-common.gradle @@ -0,0 +1,123 @@ +plugins { + id 'java-library' + id 'maven-publish' +} + +base { + archivesName = "${mod_id}-${project.name}-${minecraft_version}" +} + +java { + toolchain.languageVersion = JavaLanguageVersion.of(java_version) + withSourcesJar() + withJavadocJar() +} + +repositories { + mavenCentral() + // https://docs.gradle.org/current/userguide/declaring_repositories.html#declaring_content_exclusively_found_in_one_repository + exclusiveContent { + forRepository { + maven { + name = 'Sponge' + url = 'https://repo.spongepowered.org/repository/maven-public' + } + } + filter { includeGroupAndSubgroups('org.spongepowered') } + } + exclusiveContent { + forRepositories( + maven { + name = 'ParchmentMC' + url = 'https://maven.parchmentmc.org/' + }, + maven { + name = "NeoForge" + url = 'https://maven.neoforged.net/releases' + } + ) + filter { includeGroup('org.parchmentmc.data') } + } + maven { + name = 'BlameJared' + url = 'https://maven.blamejared.com' + } +} + +// Declare capabilities on the outgoing configurations. +// Read more about capabilities here: https://docs.gradle.org/current/userguide/component_capabilities.html#sec:declaring-additional-capabilities-for-a-local-component +['apiElements', 'runtimeElements', 'sourcesElements', 'javadocElements'].each { variant -> + configurations."$variant".outgoing { + capability("$group:${base.archivesName.get()}:$version") + capability("$group:$mod_id-${project.name}-${minecraft_version}:$version") + capability("$group:$mod_id:$version") + } + publishing.publications.configureEach { + suppressPomMetadataWarningsFor(variant) + } +} + +sourcesJar { + from(rootProject.file('LICENSE')) { + rename { "${it}_${mod_name}" } + } +} + +jar { + from(rootProject.file('LICENSE')) { + rename { "${it}_${mod_name}" } + } + + manifest { + attributes([ + 'Specification-Title' : mod_name, + 'Specification-Vendor' : mod_author, + 'Specification-Version' : project.jar.archiveVersion, + 'Implementation-Title' : project.name, + 'Implementation-Version': project.jar.archiveVersion, + 'Implementation-Vendor' : mod_author, + 'Built-On-Minecraft' : minecraft_version + ]) + } +} + +processResources { + var expandProps = [ + 'version' : version, + 'group' : project.group, //Else we target the task's group. + 'minecraft_version' : minecraft_version, + 'minecraft_version_range' : minecraft_version_range, + 'fabric_version' : fabric_version, + 'fabric_loader_version' : fabric_loader_version, + 'mod_name' : mod_name, + 'mod_author' : mod_author, + 'mod_id' : mod_id, + 'license' : license, + 'description' : project.description, + 'neoforge_version' : neoforge_version, + 'neoforge_loader_version_range': neoforge_loader_version_range, + "forge_version": forge_version, + "forge_loader_version_range": forge_loader_version_range, + 'credits' : credits, + 'java_version' : java_version + ] + + filesMatching(['pack.mcmeta', 'fabric.mod.json', 'META-INF/mods.toml', 'META-INF/neoforge.mods.toml', '*.mixins.json']) { + expand expandProps + } + inputs.properties(expandProps) +} + +publishing { + publications { + register('mavenJava', MavenPublication) { + artifactId base.archivesName.get() + from components.java + } + } + repositories { + maven { + url System.getenv('local_maven_url') + } + } +} diff --git a/buildSrc/src/main/groovy/multiloader-loader.gradle b/buildSrc/src/main/groovy/multiloader-loader.gradle new file mode 100644 index 0000000..92e2325 --- /dev/null +++ b/buildSrc/src/main/groovy/multiloader-loader.gradle @@ -0,0 +1,44 @@ +plugins { + id 'multiloader-common' +} + +configurations { + commonJava{ + canBeResolved = true + } + commonResources{ + canBeResolved = true + } +} + +dependencies { + compileOnly(project(':common')) { + capabilities { + requireCapability "$group:$mod_id" + } + } + commonJava project(path: ':common', configuration: 'commonJava') + commonResources project(path: ':common', configuration: 'commonResources') +} + +tasks.named('compileJava', JavaCompile) { + dependsOn(configurations.commonJava) + source(configurations.commonJava) +} + +processResources { + dependsOn(configurations.commonResources) + from(configurations.commonResources) +} + +tasks.named('javadoc', Javadoc).configure { + dependsOn(configurations.commonJava) + source(configurations.commonJava) +} + +tasks.named('sourcesJar', Jar) { + dependsOn(configurations.commonJava) + from(configurations.commonJava) + dependsOn(configurations.commonResources) + from(configurations.commonResources) +} diff --git a/common/build.gradle b/common/build.gradle new file mode 100644 index 0000000..ef4ac72 --- /dev/null +++ b/common/build.gradle @@ -0,0 +1,41 @@ +plugins { + id 'multiloader-common' + id 'net.neoforged.moddev' +} + +neoForge { + neoFormVersion = neo_form_version + // Automatically enable AccessTransformers if the file exists + def at = file('src/main/resources/META-INF/accesstransformer.cfg') + if (at.exists()) { + accessTransformers.add(at.absolutePath) + } + parchment { + minecraftVersion = parchment_minecraft + mappingsVersion = parchment_version + } +} + +dependencies { + compileOnly group: 'org.spongepowered', name: 'mixin', version: '0.8.5' + // fabric and neoforge both bundle mixinextras, so it is safe to use it in common + compileOnly group: 'io.github.llamalad7', name: 'mixinextras-common', version: '0.3.5' + annotationProcessor group: 'io.github.llamalad7', name: 'mixinextras-common', version: '0.3.5' +} + +configurations { + commonJava { + canBeResolved = false + canBeConsumed = true + } + commonResources { + canBeResolved = false + canBeConsumed = true + } +} + +artifacts { + commonJava sourceSets.main.java.sourceDirectories.singleFile + commonResources sourceSets.main.resources.sourceDirectories.singleFile +} + diff --git a/common/src/main/java/net/deechael/concentration/Concentration.java b/common/src/main/java/net/deechael/concentration/Concentration.java new file mode 100644 index 0000000..bab0277 --- /dev/null +++ b/common/src/main/java/net/deechael/concentration/Concentration.java @@ -0,0 +1,44 @@ +package net.deechael.concentration; + +import com.mojang.blaze3d.platform.Window; +import net.deechael.concentration.mixin.accessor.WindowAccessor; +import net.minecraft.client.Minecraft; +import net.minecraft.client.Options; + +/** + * Main class of Concentration + * + * @author DeeChael + */ +public class Concentration { + + /** + * Initialize the mod + */ + public static void init() { + ConcentrationConstants.LOGGER.info("Welcome to borderless world! Concentrate on your game!"); + } + + /** + * Toggle fullscreen mode + * + * @param options should be Minecraft vanilla options + * @param value whether fullscreen is turned on + */ + public static void toggleFullScreenMode(Options options, boolean value) { + options.fullscreen().set(value); + + Window window = Minecraft.getInstance().getWindow(); + + if (window.isFullscreen() != options.fullscreen().get()) { + window.toggleFullScreen(); + options.fullscreen().set(window.isFullscreen()); + } + + if (options.fullscreen().get()) { + ((WindowAccessor) (Object) window).setDirty(true); + window.changeFullscreenVideoMode(); + } + } + +} \ No newline at end of file diff --git a/common/src/main/java/net/deechael/concentration/ConcentrationConstants.java b/common/src/main/java/net/deechael/concentration/ConcentrationConstants.java new file mode 100644 index 0000000..6810d73 --- /dev/null +++ b/common/src/main/java/net/deechael/concentration/ConcentrationConstants.java @@ -0,0 +1,29 @@ +package net.deechael.concentration; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Class stores all the constants of Concentration + * + * @author DeeChael + */ +public final class ConcentrationConstants { + + /** + * Mod id of Concentration + */ + public static final String MOD_ID = "concentration"; + /** + * Mod name of Concentration + */ + public static final String MOD_NAME = "Concentration"; + /** + * Logger of Concentration + */ + public static final Logger LOGGER = LoggerFactory.getLogger(MOD_NAME); + + private ConcentrationConstants() { + } + +} \ No newline at end of file diff --git a/common/src/main/java/net/deechael/concentration/FullscreenMode.java b/common/src/main/java/net/deechael/concentration/FullscreenMode.java new file mode 100644 index 0000000..03dba70 --- /dev/null +++ b/common/src/main/java/net/deechael/concentration/FullscreenMode.java @@ -0,0 +1,44 @@ +package net.deechael.concentration; + +import com.mojang.serialization.Codec; +import net.minecraft.util.ByIdMap; +import net.minecraft.util.OptionEnum; +import net.minecraft.util.StringRepresentable; +import org.jetbrains.annotations.NotNull; + +import java.util.function.IntFunction; + +public enum FullscreenMode implements OptionEnum, StringRepresentable { + + BORDERLESS(0, "borderless", "concentration.option.fullscreen_mode.borderless"), + NATIVE(1, "native", "concentration.option.fullscreen_mode.native"); + + public static final Codec CODEC = StringRepresentable.fromEnum(FullscreenMode::values); + public static final IntFunction BY_ID = ByIdMap.continuous(FullscreenMode::getId, values(), ByIdMap.OutOfBoundsStrategy.ZERO); + + private final int id; + private final String serializedName; + private final String translationKey; + + FullscreenMode(int id, String serializedName, String translatableKey) { + this.id = id; + this.serializedName = serializedName; + this.translationKey = translatableKey; + } + + @Override + public int getId() { + return this.id; + } + + @Override + public @NotNull String getKey() { + return this.translationKey; + } + + @Override + public @NotNull String getSerializedName() { + return this.serializedName; + } + +} diff --git a/common/src/main/java/net/deechael/concentration/config/ConcentrationConfigScreen.java b/common/src/main/java/net/deechael/concentration/config/ConcentrationConfigScreen.java new file mode 100644 index 0000000..cfa0c71 --- /dev/null +++ b/common/src/main/java/net/deechael/concentration/config/ConcentrationConfigScreen.java @@ -0,0 +1,296 @@ +package net.deechael.concentration.config; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.OptionInstance; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.ContainerObjectSelectionList; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.gui.narration.NarratableEntry; +import net.minecraft.client.gui.narration.NarratedElementType; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.CommonComponents; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.Style; +import org.jetbrains.annotations.NotNull; + +import java.util.*; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.function.Supplier; + +public abstract class ConcentrationConfigScreen extends Screen { + + private final Screen parent; + private ConfigListWidget entries; + + protected ConcentrationConfigScreen(Component title, Screen parent) { + super(title); + this.parent = parent; + } + + @Override + protected final void init() { + entries = new ConfigListWidget(this.minecraft, width, height - 64, 32, 25); + addElements(); + addRenderableWidget(entries); + addRenderableWidget( + Button.builder(Component.translatable("concentration.option.apply"), (button) -> { + save(); + }) + .pos(this.width / 2 - 175, this.height - 27) + .size(150, 20) + .build() + ); + addRenderableWidget( + Button.builder(CommonComponents.GUI_DONE, (button) -> { + save(); + this.minecraft.setScreen(parent); + }) + .pos(this.width / 2 + 25, this.height - 27) + .size(150, 20) + .build() + ); + } + + private static class ConfigListWidget extends ContainerObjectSelectionList { + + public ConfigListWidget(Minecraft minecraftClient, int width, int height, int y, int itemHeight) { + super(minecraftClient, width, height, y, itemHeight); + } + + @Override + public int addEntry(@NotNull ConfigListEntry entry) { + return super.addEntry(entry); + } + + public int getRowWidth() { + return 400; + } + + protected int getScrollbarPosition() { + return super.getScrollbarPosition() + 32; + } + + public Style getHoveredStyle(int mouseX, int mouseY) { + Optional hovered = getChildAt(mouseX, mouseY); + return hovered.map(guiEventListener -> ((ConfigListEntry) guiEventListener).getHoveredStyle(mouseX, mouseY)).orElse(null); + } + } + + public static class ConfigListEntry extends ContainerObjectSelectionList.Entry { + + private final List buttons; + + public ConfigListEntry(List buttons) { + this.buttons = buttons; + } + + @Override + public void render(@NotNull GuiGraphics context, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) { + for (AbstractWidget widget : buttons) { + widget.setY(y); + widget.render(context, mouseX, mouseY, tickDelta); + } + } + + @Override + public List narratables() { + return buttons; + } + + @Override + public List children() { + return buttons; + } + + public Style getHoveredStyle(int mouseX, int mouseY) { + return null; + } + } + + @Override + public final void removed() { + save(); + } + + public abstract void save(); + + @Override + public void render(@NotNull GuiGraphics context, int mouseX, int mouseY, float delta) { + super.render(context, mouseX, mouseY, delta); + Style hoveredStyle = entries.getHoveredStyle(mouseX, mouseY); + if (hoveredStyle != null) { + context.renderComponentHoverEffect(this.font, hoveredStyle, mouseX, mouseY); + } + context.drawCenteredString(this.font, this.title, this.width / 2, 10, 16777215); + } + + @Override + public void renderBackground(@NotNull GuiGraphics context, int mouseX, int mouseY, float delta) { + // this.renderTransparentBackground(context); + super.renderBackground(context, mouseX, mouseY, delta); + } + + public abstract void addElements(); + + public void addOption(OptionInstance opt) { + entries.addEntry(new ConfigListEntry(Collections.singletonList(opt.createButton(this.minecraft.options, width / 2 - 155, 0, 310)))); + } + + public void addOptionsRow(OptionInstance opt, OptionInstance opt2) { + entries.addEntry(new ConfigListEntry(Arrays.asList( + opt.createButton(this.minecraft.options, width / 2 - 155, 0, 150), + opt2.createButton(this.minecraft.options, width / 2 - 155 + 160, 0, 150)))); + } + + public static class ConfigListHeader extends ConfigListEntry { + private final Component headerText; + private final Font textRenderer; + private final int width; + private final int textWidth; + private final Screen screen; + + public ConfigListHeader(Component headerText, Font textRenderer, int width, Screen screen) { + super(Collections.emptyList()); + this.headerText = headerText; + this.textRenderer = textRenderer; + this.width = width; + this.textWidth = textRenderer.width(headerText); + this.screen = screen; + } + + @Override + public void render(@NotNull GuiGraphics context, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) { + context.drawCenteredString(textRenderer, headerText, width / 2, y + 5, 16777215); + } + + private Style getStyleAt(int mouseX) { + int min = (width / 2) - (textWidth / 2); + int max = (width / 2) + (textWidth / 2); + if (mouseX >= min && mouseX <= max) { + return textRenderer.getSplitter().componentStyleAtWidth(headerText, mouseX - min); + } + return null; + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + return screen.handleComponentClicked(getStyleAt((int) mouseX)); + } + + @Override + public Style getHoveredStyle(int mouseX, int mouseY) { + Style style = getStyleAt(mouseX); + if (style != null && style.getHoverEvent() != null) { + return style; + } + return null; + } + + @Override + public List narratables() { + return Collections.singletonList(new NarratableEntry() { + @Override + public NarratableEntry.@NotNull NarrationPriority narrationPriority() { + return NarratableEntry.NarrationPriority.HOVERED; + } + + @Override + public void updateNarration(@NotNull NarrationElementOutput builder) { + builder.add(NarratedElementType.TITLE, headerText); + } + }); + } + } + + public void addHeading(Component text) { + entries.addEntry(new ConfigListHeader(text, font, width, this)); + } + + public static class ConfigListTextField extends ConfigListEntry { + private final EditBox textField; + private final int textWidth; + private final Font textRenderer; + private final int x; + + public ConfigListTextField(Font textRenderer, int x, int y, int width, int height, Component description, Supplier getter, Consumer setter, Predicate validator) { + super(Collections.singletonList(makeField(textRenderer, x, y, width, height, description))); + this.textField = (EditBox) children().getFirst(); + this.textWidth = textRenderer.width(description); + this.textRenderer = textRenderer; + this.x = x; + textField.setValue(getter.get()); + textField.setResponder(value -> { + if (validator.test(value)) { + setter.accept(value); + textField.setTextColor(14737632); + } else { + textField.setTextColor(16711680); + } + }); + } + + private static EditBox makeField(Font textRenderer, int x, int y, int width, int height, Component description) { + return new EditBox(textRenderer, x + (width / 2) + 7, y, (width / 2) - 8, height, description) { + @Override + public void updateWidgetNarration(NarrationElementOutput builder) { + builder.add(NarratedElementType.TITLE, createNarrationMessage()); // Use the narration message which includes the description + } + }; + } + + @Override + public Style getHoveredStyle(int mouseX, int mouseY) { + int max = this.x + textWidth; + if (mouseX >= this.x && mouseX <= max) { + Style style = textRenderer.getSplitter().componentStyleAtWidth(textField.getMessage(), mouseX - this.x); + if (style != null && style.getHoverEvent() != null) { + return style; + } + } + return null; + } + + @Override + public void render(@NotNull GuiGraphics drawContext, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) { + drawContext.drawString(textRenderer, textField.getMessage(), this.x, y + 5, 16777215); + super.render(drawContext, index, y, x, entryWidth, entryHeight, mouseX, mouseY, hovered, tickDelta); + } + } + + public void addTextField(Component description, Supplier getter, Consumer setter) { + addTextField(description, getter, setter, Objects::nonNull); + } + + public void addTextField(Component description, Supplier getter, Consumer setter, Predicate validator) { + entries.addEntry(new ConfigListTextField(font, width / 2 - 154, 0, 308, 18, description, getter, setter, validator)); + } + + public void addIntField(Component description, Supplier getter, Consumer setter) { + addTextField(description, () -> getter.get().toString(), value -> setter.accept(Integer.parseInt(value)), value -> { + try { + Integer.parseInt(value); + return true; + } catch (NumberFormatException e) { + return false; + } + }); + } + + public void addFloatField(Component description, Supplier getter, Consumer setter) { + addTextField(description, () -> getter.get().toString(), value -> setter.accept(Float.parseFloat(value)), value -> { + try { + Float.parseFloat(value); + return true; + } catch (NumberFormatException e) { + return false; + } + }); + } + +} diff --git a/common/src/main/java/net/deechael/concentration/config/Config.java b/common/src/main/java/net/deechael/concentration/config/Config.java new file mode 100644 index 0000000..2fb52e5 --- /dev/null +++ b/common/src/main/java/net/deechael/concentration/config/Config.java @@ -0,0 +1,13 @@ +package net.deechael.concentration.config; + +import net.deechael.concentration.FullscreenMode; + +public interface Config { + + FullscreenMode getFullscreenMode(); + + void setFullscreenMode(FullscreenMode fullscreenMode); + + void save(); + +} diff --git a/common/src/main/java/net/deechael/concentration/config/ConfigProvider.java b/common/src/main/java/net/deechael/concentration/config/ConfigProvider.java new file mode 100644 index 0000000..bebf0f4 --- /dev/null +++ b/common/src/main/java/net/deechael/concentration/config/ConfigProvider.java @@ -0,0 +1,17 @@ +package net.deechael.concentration.config; + +import java.util.ServiceLoader; + +public interface ConfigProvider { + + ConfigProvider INSTANCE = get(); + + Config ensureLoaded(); + + static ConfigProvider get() { + return ServiceLoader.load(ConfigProvider.class) + .findFirst() + .orElseThrow(); + } + +} diff --git a/common/src/main/java/net/deechael/concentration/mixin/KeyboardHandlerMixin.java b/common/src/main/java/net/deechael/concentration/mixin/KeyboardHandlerMixin.java new file mode 100644 index 0000000..ef43bc3 --- /dev/null +++ b/common/src/main/java/net/deechael/concentration/mixin/KeyboardHandlerMixin.java @@ -0,0 +1,32 @@ +package net.deechael.concentration.mixin; + +import net.deechael.concentration.Concentration; +import net.minecraft.client.KeyboardHandler; +import net.minecraft.client.Minecraft; +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; + +/** + * Make fullscreen shortcut follow Concentration function + * + * @author DeeChael + */ +@Mixin(KeyboardHandler.class) +public class KeyboardHandlerMixin { + + @Shadow + @Final + private Minecraft minecraft; + + @Inject(method = "keyPress", at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/platform/Window;toggleFullScreen()V"), cancellable = true) + public void redirect$handleFullScreenToggle(long pWindowPointer, int pKey, int pScanCode, int pAction, int pModifiers, CallbackInfo ci) { + Concentration.toggleFullScreenMode(minecraft.options, !minecraft.options.fullscreen().get()); + minecraft.options.save(); // Only keyboard shortcut needs save manually because shortcut won't automatically save + ci.cancel(); // Stop the original toggling function + } + +} diff --git a/common/src/main/java/net/deechael/concentration/mixin/VideoSettingsScreenMixin.java b/common/src/main/java/net/deechael/concentration/mixin/VideoSettingsScreenMixin.java new file mode 100644 index 0000000..f15e6cd --- /dev/null +++ b/common/src/main/java/net/deechael/concentration/mixin/VideoSettingsScreenMixin.java @@ -0,0 +1,99 @@ +package net.deechael.concentration.mixin; + +import net.deechael.concentration.Concentration; +import net.deechael.concentration.FullscreenMode; +import net.deechael.concentration.config.ConfigProvider; +import net.minecraft.client.OptionInstance; +import net.minecraft.client.Options; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.options.OptionsSubScreen; +import net.minecraft.client.gui.screens.options.VideoSettingsScreen; +import net.minecraft.network.chat.Component; +import net.minecraft.world.entity.HumanoidArm; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.Arrays; + +/** + * Make vanilla fullscreen option follow Concentration function + * + * @author DeeChael + */ +@Mixin(VideoSettingsScreen.class) +public abstract class VideoSettingsScreenMixin extends OptionsSubScreen { + + public VideoSettingsScreenMixin(Screen parent, Options options, Component component) { + super(parent, options, component); + } + + @Inject(method = "removed", at = @At("HEAD")) + private void inject$removed(CallbackInfo ci) { + this.options.save(); // fix that the options won't save when exit options screen by pressing ESC + } + + @Inject(method = "options", at = @At("HEAD"), cancellable = true) + private static void inject$options(Options options, CallbackInfoReturnable[]> cir) { + cir.setReturnValue( + new OptionInstance[]{ + options.graphicsMode(), + options.renderDistance(), + options.prioritizeChunkUpdates(), + options.simulationDistance(), + options.ambientOcclusion(), + options.framerateLimit(), + options.enableVsync(), + options.bobView(), + options.guiScale(), + options.attackIndicator(), + options.gamma(), + options.cloudStatus(), + concentration$FullscreenMode(options), + concentration$wrapperFullscreen(options), + options.particles(), + options.mipmapLevels(), + options.entityShadows(), + options.screenEffectScale(), + options.entityDistanceScaling(), + options.fovEffectScale(), + options.showAutosaveIndicator(), + options.glintSpeed(), + options.glintStrength(), + options.menuBackgroundBlurriness() + } + ); + } + + @Unique + private static OptionInstance concentration$FullscreenMode(Options options) { + return new OptionInstance<>( + "concentration.option.fullscreen_mode", + OptionInstance.noTooltip(), + OptionInstance.forOptionEnum(), + new OptionInstance.Enum<>(Arrays.asList(FullscreenMode.values()), FullscreenMode.CODEC), + ConfigProvider.INSTANCE.ensureLoaded().getFullscreenMode(), + fullscreenMode -> { + ConfigProvider.INSTANCE.ensureLoaded().setFullscreenMode(fullscreenMode); + ConfigProvider.INSTANCE.ensureLoaded().save(); // Because this option actually not saving in vanilla options, so it need save manually + if (options.fullscreen().get()) { + // If fullscreen turns on, re-toggle to changing the fullscreen mode instantly + Concentration.toggleFullScreenMode(options, true); + } + }); + } + + @Unique + private static OptionInstance concentration$wrapperFullscreen(Options options) { + // Don't put a constant to the second parameter, or else whatever fullscreen you are, when you open video settings page, the value shown is always the same + return OptionInstance.createBoolean("options.fullscreen", options.fullscreen().get(), (value) -> Concentration.toggleFullScreenMode(options, value)); + } + + @Shadow + protected abstract void addOptions(); + +} diff --git a/common/src/main/java/net/deechael/concentration/mixin/accessor/WindowAccessor.java b/common/src/main/java/net/deechael/concentration/mixin/accessor/WindowAccessor.java new file mode 100644 index 0000000..056cd24 --- /dev/null +++ b/common/src/main/java/net/deechael/concentration/mixin/accessor/WindowAccessor.java @@ -0,0 +1,34 @@ +package net.deechael.concentration.mixin.accessor; + +import com.mojang.blaze3d.platform.ScreenManager; +import com.mojang.blaze3d.platform.Window; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +/** + * Accessor make setDirty method available + * + * @author DeeChael + */ +@Mixin(Window.class) +public interface WindowAccessor { + + @Accessor + ScreenManager getScreenManager(); + + @Accessor + void setDirty(boolean value); + + @Accessor("x") + void setX(int x); + + @Accessor("y") + void setY(int y); + + @Accessor("width") + void setWidth(int width); + + @Accessor("height") + void setHeight(int height); + +} \ No newline at end of file diff --git a/common/src/main/resources/assets/concentration/lang/en_us.json b/common/src/main/resources/assets/concentration/lang/en_us.json new file mode 100644 index 0000000..0a8e188 --- /dev/null +++ b/common/src/main/resources/assets/concentration/lang/en_us.json @@ -0,0 +1,13 @@ +{ + "concentration.config.customization.enabled": "Enable Customization", + "concentration.config.customization.related": "Monitor Relative Coordinates", + "concentration.config.customization.x": "X Coordinate", + "concentration.config.customization.y": "Y Coordinate", + "concentration.config.customization.width": "Width", + "concentration.config.customization.height": "Height", + "concentration.option.apply": "Apply", + "concentration.option.fullscreen_mode": "Fullscreen Mode", + "concentration.option.fullscreen_mode.tooltip": "Fullscreen mode determines the fullscreen function using borderless fullscreen or native fullscreen.", + "concentration.option.fullscreen_mode.borderless": "Borderless", + "concentration.option.fullscreen_mode.native": "Native" +} \ No newline at end of file diff --git a/common/src/main/resources/assets/concentration/lang/ru_ru.json b/common/src/main/resources/assets/concentration/lang/ru_ru.json new file mode 100644 index 0000000..2c9988b --- /dev/null +++ b/common/src/main/resources/assets/concentration/lang/ru_ru.json @@ -0,0 +1,13 @@ +{ + "concentration.config.customization.enabled": "Включить настройку", + "concentration.config.customization.related": "Отслеживать относительные координаты", + "concentration.config.customization.x": "Координата X", + "concentration.config.customization.y": "Координата Y", + "concentration.config.customization.width": "Ширина", + "concentration.config.customization.height": "Высота", + "concentration.option.apply": "Применить", + "concentration.option.fullscreen_mode": "Полноэкранный режим", + "concentration.option.fullscreen_mode.tooltip": "Определяет функцию полноэкранного режима: безрамочный режим или нативный полноэкранный режим.", + "concentration.option.fullscreen_mode.borderless": "Безрамочный", + "concentration.option.fullscreen_mode.native": "Нативный" +} diff --git a/common/src/main/resources/assets/concentration/lang/zh_cn.json b/common/src/main/resources/assets/concentration/lang/zh_cn.json new file mode 100644 index 0000000..a344086 --- /dev/null +++ b/common/src/main/resources/assets/concentration/lang/zh_cn.json @@ -0,0 +1,13 @@ +{ + "concentration.config.customization.enabled": "启用自定义", + "concentration.config.customization.related": "显示器相对坐标", + "concentration.config.customization.x": "X 坐标", + "concentration.config.customization.y": "Y 坐标", + "concentration.config.customization.width": "宽度", + "concentration.config.customization.height": "高度", + "concentration.option.apply": "应用", + "concentration.option.fullscreen_mode": "全屏模式", + "concentration.option.fullscreen_mode.tooltip": "决定开启全屏后应使用无边框窗口全屏还是原生全屏。", + "concentration.option.fullscreen_mode.borderless": "无边框", + "concentration.option.fullscreen_mode.native": "原生" +} \ No newline at end of file diff --git a/common/src/main/resources/assets/concentration/lang/zh_tw.json b/common/src/main/resources/assets/concentration/lang/zh_tw.json new file mode 100644 index 0000000..7b9a9c2 --- /dev/null +++ b/common/src/main/resources/assets/concentration/lang/zh_tw.json @@ -0,0 +1,13 @@ +{ + "concentration.config.customization.enabled": "啟用客製化", + "concentration.config.customization.related": "顯示器相對座標", + "concentration.config.customization.x": "X 座標", + "concentration.config.customization.y": "Y 座標", + "concentration.config.customization.width": "寬度", + "concentration.config.customization.height": "高度", + "concentration.option.apply": "套用", + "concentration.option.fullscreen_mode": "全螢幕模式", + "concentration.option.fullscreen_mode.tooltip": "全螢幕模式決定了全螢幕功能要使用無邊框全螢幕還是原生全螢幕。", + "concentration.option.fullscreen_mode.borderless": "無邊框", + "concentration.option.fullscreen_mode.native": "原生" +} \ No newline at end of file diff --git a/common/src/main/resources/concentration.mixins.json b/common/src/main/resources/concentration.mixins.json new file mode 100644 index 0000000..51c6753 --- /dev/null +++ b/common/src/main/resources/concentration.mixins.json @@ -0,0 +1,18 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "net.deechael.concentration.mixin", + "refmap": "${mod_id}.refmap.json", + "compatibilityLevel": "JAVA_21", + "mixins": [], + "client": [ + "KeyboardHandlerMixin", + "VideoSettingsScreenMixin", + "accessor.WindowAccessor" + ], + "server": [], + "injectors": { + "defaultRequire": 1 + } +} + diff --git a/common/src/main/resources/logo.png b/common/src/main/resources/logo.png new file mode 100644 index 0000000..f5cf53c Binary files /dev/null and b/common/src/main/resources/logo.png differ diff --git a/common/src/main/resources/pack.mcmeta b/common/src/main/resources/pack.mcmeta new file mode 100644 index 0000000..5d43391 --- /dev/null +++ b/common/src/main/resources/pack.mcmeta @@ -0,0 +1,6 @@ +{ + "pack": { + "description": "${mod_name} Resources", + "pack_format": 8 + } +} \ No newline at end of file diff --git a/fabric/build.gradle b/fabric/build.gradle new file mode 100644 index 0000000..ebe191b --- /dev/null +++ b/fabric/build.gradle @@ -0,0 +1,49 @@ +plugins { + id 'multiloader-loader' + id 'fabric-loom' +} + +repositories { + maven { + url = "https://api.modrinth.com/maven" + } +} + +dependencies { + minecraft "com.mojang:minecraft:${minecraft_version}" + mappings loom.layered { + officialMojangMappings() + parchment("org.parchmentmc.data:parchment-${parchment_minecraft}:${parchment_version}@zip") + } + modImplementation "net.fabricmc:fabric-loader:${fabric_loader_version}" + modImplementation "net.fabricmc.fabric-api:fabric-api:${fabric_version}" + + modImplementation "maven.modrinth:modmenu:11.0.0" + modImplementation "maven.modrinth:sodium:mc1.21-0.5.11" + modImplementation "maven.modrinth:sodium:mc1.21-0.6.0-beta.2-fabric" + modImplementation "maven.modrinth:vulkanmod:0.4.9-fabric,1.21" +} + +loom { + def aw = project(':common').file("src/main/resources/${mod_id}.accesswidener") + if (aw.exists()) { + accessWidenerPath.set(aw) + } + mixin { + defaultRefmapName.set("${mod_id}.refmap.json") + } + runs { + client { + client() + setConfigName('Fabric Client') + ideConfigGenerated(true) + runDir('runs/client') + } + server { + server() + setConfigName('Fabric Server') + ideConfigGenerated(true) + runDir('runs/server') + } + } +} \ No newline at end of file diff --git a/fabric/src/main/java/net/deechael/concentration/fabric/ConcentrationFabric.java b/fabric/src/main/java/net/deechael/concentration/fabric/ConcentrationFabric.java new file mode 100644 index 0000000..c4ad731 --- /dev/null +++ b/fabric/src/main/java/net/deechael/concentration/fabric/ConcentrationFabric.java @@ -0,0 +1,18 @@ +package net.deechael.concentration.fabric; + +import net.deechael.concentration.Concentration; +import net.fabricmc.api.ClientModInitializer; + +/** + * Main class for fabric version of Concentration + * + * @author DeeChael + */ +public class ConcentrationFabric implements ClientModInitializer { + + @Override + public void onInitializeClient() { + Concentration.init(); + } + +} diff --git a/fabric/src/main/java/net/deechael/concentration/fabric/ConcentrationFabricCaching.java b/fabric/src/main/java/net/deechael/concentration/fabric/ConcentrationFabricCaching.java new file mode 100644 index 0000000..07e718b --- /dev/null +++ b/fabric/src/main/java/net/deechael/concentration/fabric/ConcentrationFabricCaching.java @@ -0,0 +1,15 @@ +package net.deechael.concentration.fabric; + +public class ConcentrationFabricCaching { + + public static long lastMonitor = -1; + + public static boolean cachedSize = false; + public static boolean cachedPos = false; + public static boolean cacheSizeLock = false; + + public static int cachedX = 0; + public static int cachedY = 0; + public static int cachedWidth = 0; + public static int cachedHeight = 0; +} diff --git a/fabric/src/main/java/net/deechael/concentration/fabric/ConcentrationFabricMixinPlugin.java b/fabric/src/main/java/net/deechael/concentration/fabric/ConcentrationFabricMixinPlugin.java new file mode 100644 index 0000000..3088c1a --- /dev/null +++ b/fabric/src/main/java/net/deechael/concentration/fabric/ConcentrationFabricMixinPlugin.java @@ -0,0 +1,60 @@ +package net.deechael.concentration.fabric; + +import net.fabricmc.loader.api.FabricLoader; +import org.objectweb.asm.tree.ClassNode; +import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin; +import org.spongepowered.asm.mixin.extensibility.IMixinInfo; + +import java.util.List; +import java.util.Set; + +/** + * Mixin plugin to make sure that sodium support only turned on when sodium installed + * + * @author DeeChael + */ +public class ConcentrationFabricMixinPlugin implements IMixinConfigPlugin { + + @Override + public void onLoad(String mixinPackage) { + } + + @Override + public String getRefMapperConfig() { + return null; + } + + @Override + public boolean shouldApplyMixin(String targetClassName, String mixinClassName) { + if (FabricLoader.getInstance().isModLoaded("vulkanmod")) { + return mixinClassName.equals("net.deechael.concentration.fabric.mixin.VulkanWindowMixin") || + mixinClassName.equals("net.deechael.concentration.fabric.mixin.OptionsMixin") || + mixinClassName.equals("net.deechael.concentration.fabric.mixin.GLFWMixin"); + } else { + return checkSodium(mixinClassName) || mixinClassName.equals("net.deechael.concentration.fabric.mixin.WindowMixin"); + } + } + + @Override + public void acceptTargets(Set myTargets, Set otherTargets) { + } + + @Override + public List getMixins() { + return null; + } + + @Override + public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { + } + + @Override + public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { + } + + private static boolean checkSodium(String mixinClassName) { + return "net.deechael.concentration.fabric.mixin.SodiumVideoOptionsScreenMixin".equals(mixinClassName) + && FabricLoader.getInstance().isModLoaded("sodium"); + } + +} diff --git a/fabric/src/main/java/net/deechael/concentration/fabric/compat/ModMenuCompat.java b/fabric/src/main/java/net/deechael/concentration/fabric/compat/ModMenuCompat.java new file mode 100644 index 0000000..97b138f --- /dev/null +++ b/fabric/src/main/java/net/deechael/concentration/fabric/compat/ModMenuCompat.java @@ -0,0 +1,53 @@ +package net.deechael.concentration.fabric.compat; + +import com.terraformersmc.modmenu.api.ConfigScreenFactory; +import com.terraformersmc.modmenu.api.ModMenuApi; +import net.deechael.concentration.Concentration; +import net.deechael.concentration.ConcentrationConstants; +import net.deechael.concentration.config.ConcentrationConfigScreen; +import net.deechael.concentration.fabric.config.ConcentrationConfigFabric; +import net.minecraft.client.Minecraft; +import net.minecraft.client.OptionInstance; +import net.minecraft.network.chat.Component; + +public class ModMenuCompat implements ModMenuApi { + + @Override + public ConfigScreenFactory getModConfigScreenFactory() { + return parent -> { + ConcentrationConfigFabric configHandler = ConcentrationConfigFabric.getInstance(); + + return new ConcentrationConfigScreen(Component.literal(ConcentrationConstants.MOD_NAME), parent) { + @Override + public void save() { + configHandler.save(); + Concentration.toggleFullScreenMode(Minecraft.getInstance().options, Minecraft.getInstance().options.fullscreen().get()); + } + + @Override + public void addElements() { + addOption(OptionInstance.createBoolean("concentration.config.customization.enabled", + configHandler.customized, + value -> configHandler.customized = value)); + addOption(OptionInstance.createBoolean("concentration.config.customization.related", + configHandler.related, + value -> configHandler.related = value)); + + addIntField(Component.translatable("concentration.config.customization.x"), + () -> configHandler.x, + value -> configHandler.x = value); + addIntField(Component.translatable("concentration.config.customization.y"), + () -> configHandler.y, + value -> configHandler.y = value); + addIntField(Component.translatable("concentration.config.customization.width"), + () -> configHandler.width, + value -> configHandler.width = value > 0 ? value : 1); + addIntField(Component.translatable("concentration.config.customization.height"), + () -> configHandler.height, + value -> configHandler.height = value > 0 ? value : 1); + } + }; + }; + } + +} diff --git a/fabric/src/main/java/net/deechael/concentration/fabric/config/ConcentrationConfigFabric.java b/fabric/src/main/java/net/deechael/concentration/fabric/config/ConcentrationConfigFabric.java new file mode 100644 index 0000000..326ad93 --- /dev/null +++ b/fabric/src/main/java/net/deechael/concentration/fabric/config/ConcentrationConfigFabric.java @@ -0,0 +1,65 @@ +package net.deechael.concentration.fabric.config; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import net.deechael.concentration.FullscreenMode; +import net.deechael.concentration.config.Config; +import net.fabricmc.loader.api.FabricLoader; + +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Path; + +public class ConcentrationConfigFabric implements Config { + + private static final Path configFile = FabricLoader.getInstance().getConfigDir().resolve("concentration.json"); + + private ConcentrationConfigFabric() { + } + + private static ConcentrationConfigFabric INSTANCE = null; + + public static ConcentrationConfigFabric getInstance() { + if (INSTANCE == null) { + Gson gson = new Gson(); + try (FileReader reader = new FileReader(configFile.toFile())) { + INSTANCE = gson.fromJson(reader, ConcentrationConfigFabric.class); + } catch (IOException ignored) { + } + if (INSTANCE == null) { + INSTANCE = new ConcentrationConfigFabric(); + INSTANCE.save(); + } + } + return INSTANCE; + } + + public boolean customized = false; + public boolean related = false; + public int x = 0; + public int y = 0; + public int width = 800; + public int height = 600; + public FullscreenMode fullscreen = FullscreenMode.BORDERLESS; + + @Override + public FullscreenMode getFullscreenMode() { + return this.fullscreen; + } + + @Override + public void setFullscreenMode(FullscreenMode fullscreenMode) { + this.fullscreen = fullscreenMode; + } + + @Override + public void save() { + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + try (FileWriter writer = new FileWriter(configFile.toFile())) { + gson.toJson(this, ConcentrationConfigFabric.class, writer); + } catch (IOException ignored) { + } + } + +} diff --git a/fabric/src/main/java/net/deechael/concentration/fabric/config/FabricConfigProvider.java b/fabric/src/main/java/net/deechael/concentration/fabric/config/FabricConfigProvider.java new file mode 100644 index 0000000..1c2dc69 --- /dev/null +++ b/fabric/src/main/java/net/deechael/concentration/fabric/config/FabricConfigProvider.java @@ -0,0 +1,13 @@ +package net.deechael.concentration.fabric.config; + +import net.deechael.concentration.config.Config; +import net.deechael.concentration.config.ConfigProvider; + +public class FabricConfigProvider implements ConfigProvider { + + @Override + public Config ensureLoaded() { + return ConcentrationConfigFabric.getInstance(); + } + +} diff --git a/fabric/src/main/java/net/deechael/concentration/fabric/mixin/GLFWMixin.java b/fabric/src/main/java/net/deechael/concentration/fabric/mixin/GLFWMixin.java new file mode 100644 index 0000000..9b3ff1b --- /dev/null +++ b/fabric/src/main/java/net/deechael/concentration/fabric/mixin/GLFWMixin.java @@ -0,0 +1,157 @@ +package net.deechael.concentration.fabric.mixin; + +import com.mojang.blaze3d.platform.Monitor; +import com.mojang.blaze3d.platform.VideoMode; +import com.mojang.blaze3d.platform.Window; +import net.deechael.concentration.ConcentrationConstants; +import net.deechael.concentration.FullscreenMode; +import net.deechael.concentration.fabric.ConcentrationFabricCaching; +import net.deechael.concentration.fabric.config.ConcentrationConfigFabric; +import net.deechael.concentration.mixin.accessor.WindowAccessor; +import net.minecraft.client.Minecraft; +import org.lwjgl.glfw.GLFW; +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; + +import static org.lwjgl.system.Checks.CHECKS; +import static org.lwjgl.system.Checks.check; +import static org.lwjgl.system.JNI.invokePPV; + +@Mixin(value = GLFW.class, remap = false) +public class GLFWMixin { + + @Inject(method = "glfwSetWindowMonitor", at = @At("HEAD"), cancellable = true) + private static void inject$glfwSetWindowMonitor(long window, long monitor, int xpos, int ypos, int width, int height, int refreshRate, CallbackInfo ci) { + ConcentrationConstants.LOGGER.info("================= [Concentration Start] ================="); + ConcentrationConstants.LOGGER.info("Trying to modify window monitor"); + + // Monitor is 0 means the game is windowed mode, so the expression means if is toggling to fullscreen + if (monitor != 0L) { + ConcentrationConstants.LOGGER.info("Modifying window size limits"); + GLFW.glfwSetWindowSizeLimits(window, 0, 0, width, height); + } + + // Because whether in fullscreen mode or windowed mode + // The final step is same + // So I extracted the value then execute the final step + int finalWidth; + int finalHeight; + + int finalX; + int finalY; + + Window windowInstance = Minecraft.getInstance().getWindow(); + WindowAccessor accessor = (WindowAccessor) (Object) windowInstance; + + if (windowInstance != null && windowInstance.isFullscreen()) { + ConcentrationConfigFabric config = ConcentrationConfigFabric.getInstance(); + + // If the game started with fullscreen mode, when switching to windowed mode, it will be forced to move to the primary monitor + // Though size and position isn't be set at initialization, but I think the window should be at the initial monitor + // So save the monitor and use the monitor value when the size isn't cached + ConcentrationFabricCaching.lastMonitor = monitor; + + // Lock caching, because when switching back, the window will be once resized to the maximum value and the cache value will be wrong + // Position won't be affected, so it doesn't need lock + ConcentrationFabricCaching.cacheSizeLock = true; + ConcentrationConstants.LOGGER.info("Locked size caching"); + + if (config.fullscreen == FullscreenMode.NATIVE) { + ConcentrationConstants.LOGGER.info("Fullscreen mode is native, apply now!"); + if (monitor == 0L) + monitor = windowInstance.findBestMonitor().getMonitor(); + finalExecute(window, monitor, xpos, ypos, width, height, -1); + ConcentrationConstants.LOGGER.info("================= [Concentration End] ================="); + return; + } + + ConcentrationConstants.LOGGER.info("Trying to switch to borderless fullscreen mode"); + + // Get the monitor the user want to use and get the relative position in the system + // The monitor is always non-null because when switching fullscreen mode, there must be a monitor to put the window + Monitor monitorInstance = accessor.getScreenManager().getMonitor(monitor); + ConcentrationConstants.LOGGER.info("Current fullscreen monitor is {}", monitor); + + // Remove the title bar to prevent that user can see the title bar if they put their monitors vertically connected + GLFW.glfwSetWindowAttrib(window, GLFW.GLFW_DECORATED, GLFW.GLFW_FALSE); + ConcentrationConstants.LOGGER.info("Trying to remove the title bar"); + + if (ConcentrationConfigFabric.getInstance().customized) { + + ConcentrationConstants.LOGGER.info("Customization enabled, so replace the fullscreen size with customized size"); + finalX = config.x + (config.related ? monitorInstance.getX() : 0); + finalY = config.y - (config.height == height ? 1 : 0) + (config.related ? monitorInstance.getY() : 0); + finalWidth = config.width; + finalHeight = config.height + (config.height == height ? 1 : 0); + } else { + // If we make the window not decorated and set the window size exactly the same with the screen size, it will become native fullscreen mode + // to prevent this, I enlarge the height by 1 pixel and move up the window by 1 pixel which won't affect anything (unless you have a screen + // which is added above the monitor which holds the game) and will have a good experience + // Actually this is a little bit dirty, needs to find a better way to solve it + finalX = monitorInstance.getX(); + finalY = monitorInstance.getY() - 1; + finalWidth = width; + finalHeight = height + 1; + } + + accessor.setX(finalX); + accessor.setY(finalY); + accessor.setWidth(finalWidth); + accessor.setHeight(finalHeight); + } else { + ConcentrationConstants.LOGGER.info("Trying to switch to windowed mode"); + + // Re-add the title bar so user can move the window and minimize, maximize and close the window + ConcentrationConstants.LOGGER.info("Trying to add title bar back"); + GLFW.glfwSetWindowAttrib(window, GLFW.GLFW_DECORATED, GLFW.GLFW_TRUE); + + ConcentrationConstants.LOGGER.info("Trying to use cached value to resize the window"); + + // Make sure that Concentration has cached position and size, because position size won't be cached when the game starting in fullscreen mode + finalWidth = ConcentrationFabricCaching.cachedSize ? ConcentrationFabricCaching.cachedWidth : width; + finalHeight = ConcentrationFabricCaching.cachedSize ? ConcentrationFabricCaching.cachedHeight : height; + + // To make sure that even starting with fullscreen mode can also make the window stay at the current monitor + // So I set two ways to ensure the position + if (ConcentrationFabricCaching.cachedPos) { + // If Concentration cached the pos, use the cached value + finalX = ConcentrationFabricCaching.cachedX; + finalY = ConcentrationFabricCaching.cachedY; + } else if (ConcentrationFabricCaching.lastMonitor != -1) { + // or else maybe the game started with fullscreen mode, so I don't need to care about the size + // only need to make sure that the position is in the correct monitor + Monitor monitorInstance = accessor.getScreenManager().getMonitor(ConcentrationFabricCaching.lastMonitor); + VideoMode videoMode = monitorInstance.getCurrentMode(); + finalX = (videoMode.getWidth() - finalWidth) / 2; + finalY = (videoMode.getHeight() - finalHeight) / 2; + } else { + // if both value are missed, use the default value to prevent errors + finalX = xpos; + finalY = ypos; + } + + // Unlock caching, because user can change the window size now + ConcentrationFabricCaching.cacheSizeLock = false; + ConcentrationConstants.LOGGER.info("Unlocked size caching"); + } + + ConcentrationConstants.LOGGER.info("Window size: {}, {}, position: {}, {}", finalWidth, finalHeight, finalX, finalY); + + ConcentrationConstants.LOGGER.info("Trying to resize and reposition the window"); + finalExecute(window, 0L, finalX, finalY, finalWidth, finalHeight, -1); + + ConcentrationConstants.LOGGER.info("================= [Concentration End] ================="); + ci.cancel(); + } + + private static void finalExecute(long window, long monitor, int xpos, int ypos, int width, int height, int refreshRate) { + long __functionAddress = GLFW.Functions.SetWindowMonitor; + if (CHECKS) { + check(window); + } + invokePPV(window, monitor, xpos, ypos, width, height, refreshRate, __functionAddress); + } + +} diff --git a/fabric/src/main/java/net/deechael/concentration/fabric/mixin/OptionsMixin.java b/fabric/src/main/java/net/deechael/concentration/fabric/mixin/OptionsMixin.java new file mode 100644 index 0000000..2eef7ef --- /dev/null +++ b/fabric/src/main/java/net/deechael/concentration/fabric/mixin/OptionsMixin.java @@ -0,0 +1,56 @@ +package net.deechael.concentration.fabric.mixin; + +import net.deechael.concentration.Concentration; +import net.deechael.concentration.FullscreenMode; +import net.deechael.concentration.fabric.config.ConcentrationConfigFabric; +import net.minecraft.network.chat.Component; +import net.vulkanmod.config.option.CyclingOption; +import net.vulkanmod.config.option.Option; +import net.vulkanmod.config.option.Options; +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.ModifyArgs; +import org.spongepowered.asm.mixin.injection.invoke.arg.Args; + +@Mixin(Options.class) +public class OptionsMixin { + + @Shadow + static net.minecraft.client.Options minecraftOptions; + + @SuppressWarnings("unchecked") + @ModifyArgs(method = "getVideoOpts", remap = false, at = @At(value = "INVOKE", target = "Lnet/vulkanmod/config/gui/OptionBlock;(Ljava/lang/String;[Lnet/vulkanmod/config/option/Option;)V")) + private static void modifyArg$getVideoOpts(Args args) { + Option[] options = args.get(1); + if (options.length == 6) { + Option[] newOptions = new Option[6]; + newOptions[0] = options[0]; + newOptions[1] = options[1]; + newOptions[2] = new CyclingOption<>( + Component.translatable("concentration.option.fullscreen_mode"), + FullscreenMode.values(), + value -> { + ConcentrationConfigFabric.getInstance().fullscreen = value; + ConcentrationConfigFabric.getInstance().save(); + if (minecraftOptions.fullscreen().get()) { + // If fullscreen turns on, re-toggle to changing the fullscreen mode instantly + Concentration.toggleFullScreenMode(minecraftOptions, true); + } + }, + () -> ConcentrationConfigFabric.getInstance().fullscreen + ).setTranslator(fullscreenMode -> Component.translatable(fullscreenMode.getKey())); + newOptions[3] = options[3]; + newOptions[4] = options[4]; + newOptions[5] = options[5]; + + Option fullscreenOption = (Option) newOptions[3]; + fullscreenOption.setOnApply(value -> { + Concentration.toggleFullScreenMode(minecraftOptions, value); + }); + + args.set(1, newOptions); + } + } + +} \ No newline at end of file diff --git a/fabric/src/main/java/net/deechael/concentration/fabric/mixin/SodiumVideoOptionsScreenMixin.java b/fabric/src/main/java/net/deechael/concentration/fabric/mixin/SodiumVideoOptionsScreenMixin.java new file mode 100644 index 0000000..94a52bc --- /dev/null +++ b/fabric/src/main/java/net/deechael/concentration/fabric/mixin/SodiumVideoOptionsScreenMixin.java @@ -0,0 +1,84 @@ +package net.deechael.concentration.fabric.mixin; + +import com.google.common.collect.ImmutableList; +import net.caffeinemc.mods.sodium.client.gui.SodiumGameOptionPages; +import net.caffeinemc.mods.sodium.client.gui.options.Option; +import net.caffeinemc.mods.sodium.client.gui.options.OptionGroup; +import net.caffeinemc.mods.sodium.client.gui.options.OptionImpl; +import net.caffeinemc.mods.sodium.client.gui.options.control.CyclingControl; +import net.caffeinemc.mods.sodium.client.gui.options.control.TickBoxControl; +import net.caffeinemc.mods.sodium.client.gui.options.storage.MinecraftOptionsStorage; +import net.deechael.concentration.Concentration; +import net.deechael.concentration.FullscreenMode; +import net.deechael.concentration.fabric.config.ConcentrationConfigFabric; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.contents.TranslatableContents; +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.ModifyArg; + +import java.util.ArrayList; +import java.util.List; + +/** + * Hooks sodium options to make sure that changing fullscreen behaviour will use Concentration function instead of vanilla function + * + * @author DeeChael + */ +@Mixin(SodiumGameOptionPages.class) +public class SodiumVideoOptionsScreenMixin { + + @Shadow(remap = false) + @Final + private static MinecraftOptionsStorage vanillaOpts; + + @ModifyArg(method = "general", at = @At(value = "INVOKE", target = "Lnet/caffeinemc/mods/sodium/client/gui/options/OptionPage;(Lnet/minecraft/network/chat/Component;Lcom/google/common/collect/ImmutableList;)V"), index = 1) + private static ImmutableList inject$general(ImmutableList groups) { + List newGroups = new ArrayList<>(); + + for (OptionGroup group : groups) { + OptionGroup.Builder builder = OptionGroup.createBuilder(); + for (Option option : group.getOptions()) { + if (option.getName().getContents() instanceof TranslatableContents translatableContents) { + if (translatableContents.getKey().equals("options.fullscreen")) { + builder.add( + OptionImpl.createBuilder(FullscreenMode.class, vanillaOpts) + .setName(Component.translatable("concentration.option.fullscreen_mode")) + .setTooltip(Component.translatable("concentration.option.fullscreen_mode.tooltip")) + .setControl((opt) -> new CyclingControl<>(opt, FullscreenMode.class, new Component[]{ + Component.translatable("concentration.option.fullscreen_mode.borderless"), + Component.translatable("concentration.option.fullscreen_mode.native") + })) + .setBinding((options, value) -> { + ConcentrationConfigFabric.getInstance().fullscreen = value; + ConcentrationConfigFabric.getInstance().save(); + if (options.fullscreen().get()) { + // If fullscreen turns on, re-toggle to changing the fullscreen mode instantly + Concentration.toggleFullScreenMode(options, true); + } + }, + (options) -> ConcentrationConfigFabric.getInstance().fullscreen + ) + .build() + ).add( + OptionImpl.createBuilder(boolean.class, vanillaOpts) + .setName(Component.translatable("options.fullscreen")) + .setTooltip(Component.translatable("sodium.options.fullscreen.tooltip")) + .setControl(TickBoxControl::new) + .setBinding(Concentration::toggleFullScreenMode, (options) -> options.fullscreen().get()) + .build() + ); + continue; + } + } + builder.add(option); + } + newGroups.add(builder.build()); + } + + return ImmutableList.copyOf(newGroups); + } + +} diff --git a/fabric/src/main/java/net/deechael/concentration/fabric/mixin/VulkanWindowMixin.java b/fabric/src/main/java/net/deechael/concentration/fabric/mixin/VulkanWindowMixin.java new file mode 100644 index 0000000..724d1bb --- /dev/null +++ b/fabric/src/main/java/net/deechael/concentration/fabric/mixin/VulkanWindowMixin.java @@ -0,0 +1,47 @@ +package net.deechael.concentration.fabric.mixin; + +import com.mojang.blaze3d.platform.Window; +import net.deechael.concentration.ConcentrationConstants; +import net.deechael.concentration.fabric.ConcentrationFabricCaching; +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; + +/** + * Borderless implementation + * + * @author DeeChael + */ +@Mixin(value = Window.class, priority = 2000) +public abstract class VulkanWindowMixin { + + @Shadow + private boolean fullscreen; + + @Inject(method = "onMove", at = @At("HEAD")) + private void inject$onMove$head(long window, int x, int y, CallbackInfo ci) { + if (!this.fullscreen) { + if (!ConcentrationFabricCaching.cachedPos) { + ConcentrationConstants.LOGGER.info("Window position has been cached"); + } + ConcentrationFabricCaching.cachedPos = true; + ConcentrationFabricCaching.cachedX = x; + ConcentrationFabricCaching.cachedY = y; + } + } + + @Inject(method = "onResize", at = @At("HEAD")) + private void inject$onResize$head(long window, int width, int height, CallbackInfo ci) { + if (!this.fullscreen && !ConcentrationFabricCaching.cacheSizeLock) { + if (!ConcentrationFabricCaching.cachedSize) { + ConcentrationConstants.LOGGER.info("Window size has been cached"); + } + ConcentrationFabricCaching.cachedSize = true; + ConcentrationFabricCaching.cachedWidth = width; + ConcentrationFabricCaching.cachedHeight = height; + } + } + +} \ No newline at end of file diff --git a/fabric/src/main/java/net/deechael/concentration/fabric/mixin/WindowMixin.java b/fabric/src/main/java/net/deechael/concentration/fabric/mixin/WindowMixin.java new file mode 100644 index 0000000..31e4b01 --- /dev/null +++ b/fabric/src/main/java/net/deechael/concentration/fabric/mixin/WindowMixin.java @@ -0,0 +1,190 @@ +package net.deechael.concentration.fabric.mixin; + +import com.mojang.blaze3d.platform.Monitor; +import com.mojang.blaze3d.platform.ScreenManager; +import com.mojang.blaze3d.platform.VideoMode; +import com.mojang.blaze3d.platform.Window; +import net.deechael.concentration.ConcentrationConstants; +import net.deechael.concentration.FullscreenMode; +import net.deechael.concentration.fabric.ConcentrationFabricCaching; +import net.deechael.concentration.fabric.config.ConcentrationConfigFabric; +import org.lwjgl.glfw.GLFW; +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.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** + * Borderless implementation + * + * @author DeeChael + */ +@Mixin(Window.class) +public abstract class WindowMixin { + + @Shadow + private boolean fullscreen; + + @Shadow + @Final + private ScreenManager screenManager; + + @Shadow + private int x; + @Shadow + private int y; + @Shadow + private int width; + @Shadow + private int height; + + @Inject(method = "onMove", at = @At("HEAD")) + private void inject$onMove$head(long window, int x, int y, CallbackInfo ci) { + if (!this.fullscreen) { + if (!ConcentrationFabricCaching.cachedPos) { + ConcentrationConstants.LOGGER.info("Window position has been cached"); + } + ConcentrationFabricCaching.cachedPos = true; + ConcentrationFabricCaching.cachedX = x; + ConcentrationFabricCaching.cachedY = y; + } + } + + @Inject(method = "onResize", at = @At("HEAD")) + private void inject$onResize$head(long window, int width, int height, CallbackInfo ci) { + if (!this.fullscreen && !ConcentrationFabricCaching.cacheSizeLock) { + if (!ConcentrationFabricCaching.cachedSize) { + ConcentrationConstants.LOGGER.info("Window size has been cached"); + } + ConcentrationFabricCaching.cachedSize = true; + ConcentrationFabricCaching.cachedWidth = width; + ConcentrationFabricCaching.cachedHeight = height; + } + } + + @Redirect(method = "setMode", at = @At(value = "INVOKE", remap = false, target = "Lorg/lwjgl/glfw/GLFW;glfwSetWindowMonitor(JJIIIII)V")) + private void redirect$glfwSetWindowMonitor(long window, long monitor, int xpos, int ypos, int width, int height, int refreshRate) { + ConcentrationConstants.LOGGER.info("================= [Concentration Start] ================="); + ConcentrationConstants.LOGGER.info("Trying to modify window monitor"); + + // Monitor is 0 means the game is windowed mode, so the expression means if is toggling to fullscreen + if (monitor != 0L) { + ConcentrationConstants.LOGGER.info("Modifying window size limits"); + GLFW.glfwSetWindowSizeLimits(window, 0, 0, width, height); + } + + // Because whether in fullscreen mode or windowed mode + // The final step is same + // So I extracted the value then execute the final step + int finalWidth; + int finalHeight; + + int finalX; + int finalY; + + if (this.fullscreen) { + ConcentrationConfigFabric config = ConcentrationConfigFabric.getInstance(); + + // If the game started with fullscreen mode, when switching to windowed mode, it will be forced to move to the primary monitor + // Though size and position isn't be set at initialization, but I think the window should be at the initial monitor + // So save the monitor and use the monitor value when the size isn't cached + ConcentrationFabricCaching.lastMonitor = monitor; + + // Lock caching, because when switching back, the window will be once resized to the maximum value and the cache value will be wrong + // Position won't be affected, so it doesn't need lock + ConcentrationFabricCaching.cacheSizeLock = true; + ConcentrationConstants.LOGGER.info("Locked size caching"); + + if (config.fullscreen == FullscreenMode.NATIVE) { + ConcentrationConstants.LOGGER.info("Fullscreen mode is native, apply now!"); + GLFW.glfwSetWindowMonitor(window, monitor, xpos, ypos, width, height, -1); + ConcentrationConstants.LOGGER.info("================= [Concentration End] ================="); + return; + } + + ConcentrationConstants.LOGGER.info("Trying to switch to borderless fullscreen mode"); + + // Get the monitor the user want to use and get the relative position in the system + // The monitor is always non-null because when switching fullscreen mode, there must be a monitor to put the window + Monitor monitorInstance = this.screenManager.getMonitor(monitor); + ConcentrationConstants.LOGGER.info("Current fullscreen monitor is {}", monitor); + + // Remove the title bar to prevent that user can see the title bar if they put their monitors vertically connected + GLFW.glfwSetWindowAttrib(window, GLFW.GLFW_DECORATED, GLFW.GLFW_FALSE); + ConcentrationConstants.LOGGER.info("Trying to remove the title bar"); + + if (ConcentrationConfigFabric.getInstance().customized) { + ConcentrationConstants.LOGGER.info("Customization enabled, so replace the fullscreen size with customized size"); + finalX = config.x + (config.related ? monitorInstance.getX() : 0); + finalY = config.y - (config.height == height ? 1 : 0) + (config.related ? monitorInstance.getY() : 0); + finalWidth = config.width; + finalHeight = config.height + (config.height == height ? 1 : 0); + } else { + // If we make the window not decorated and set the window size exactly the same with the screen size, it will become native fullscreen mode + // to prevent this, I enlarge the height by 1 pixel and move up the window by 1 pixel which won't affect anything (unless you have a screen + // which is added above the monitor which holds the game) and will have a good experience + // Actually this is a little bit dirty, needs to find a better way to solve it + finalX = monitorInstance.getX(); + finalY = monitorInstance.getY() - 1; + finalWidth = width; + finalHeight = height + 1; + } + + this.x = finalX; + this.y = finalY; + this.width = finalWidth; + this.height = finalHeight; + } else { + ConcentrationConstants.LOGGER.info("Trying to switch to windowed mode"); + + // Re-add the title bar so user can move the window and minimize, maximize and close the window + ConcentrationConstants.LOGGER.info("Trying to add title bar back"); + GLFW.glfwSetWindowAttrib(window, GLFW.GLFW_DECORATED, GLFW.GLFW_TRUE); + + ConcentrationConstants.LOGGER.info("Trying to use cached value to resize the window"); + + // Make sure that Concentration has cached position and size, because position size won't be cached when the game starting in fullscreen mode + finalWidth = ConcentrationFabricCaching.cachedSize ? ConcentrationFabricCaching.cachedWidth : width; + finalHeight = ConcentrationFabricCaching.cachedSize ? ConcentrationFabricCaching.cachedHeight : height; + + // To make sure that even starting with fullscreen mode can also make the window stay at the current monitor + // So I set two ways to ensure the position + if (ConcentrationFabricCaching.cachedPos) { + // If Concentration cached the pos, use the cached value + finalX = ConcentrationFabricCaching.cachedX; + finalY = ConcentrationFabricCaching.cachedY; + } else if (ConcentrationFabricCaching.lastMonitor != -1) { + // or else maybe the game started with fullscreen mode, so I don't need to care about the size + // only need to make sure that the position is in the correct monitor + Monitor monitorInstance = this.screenManager.getMonitor(ConcentrationFabricCaching.lastMonitor); + VideoMode videoMode = monitorInstance.getCurrentMode(); + finalX = (videoMode.getWidth() - finalWidth) / 2; + finalY = (videoMode.getHeight() - finalHeight) / 2; + } else { + // if both value are missed, use the default value to prevent errors + finalX = xpos; + finalY = ypos; + } + + // Unlock caching, because user can change the window size now + ConcentrationFabricCaching.cacheSizeLock = false; + ConcentrationConstants.LOGGER.info("Unlocked size caching"); + } + + ConcentrationConstants.LOGGER.info("Window size: {}, {}, position: {}, {}", finalWidth, finalHeight, finalX, finalY); + + ConcentrationConstants.LOGGER.info("Trying to resize and reposition the window"); + GLFW.glfwSetWindowMonitor(window, 0L, finalX, finalY, finalWidth, finalHeight, -1); + + ConcentrationConstants.LOGGER.info("================= [Concentration End] ================="); + } + + @Redirect(method = "setMode", at = @At(value = "INVOKE", remap = false, target = "Lorg/lwjgl/glfw/GLFW;glfwGetWindowMonitor(J)J")) + private long redirect$glfwGetWindowMonitor(long window) { + return 1L; + } + +} \ No newline at end of file diff --git a/fabric/src/main/resources/META-INF/services/net.deechael.concentration.config.ConfigProvider b/fabric/src/main/resources/META-INF/services/net.deechael.concentration.config.ConfigProvider new file mode 100644 index 0000000..1422dcf --- /dev/null +++ b/fabric/src/main/resources/META-INF/services/net.deechael.concentration.config.ConfigProvider @@ -0,0 +1 @@ +net.deechael.concentration.fabric.config.FabricConfigProvider \ No newline at end of file diff --git a/fabric/src/main/resources/concentration.fabric.mixins.json b/fabric/src/main/resources/concentration.fabric.mixins.json new file mode 100644 index 0000000..ae41bb6 --- /dev/null +++ b/fabric/src/main/resources/concentration.fabric.mixins.json @@ -0,0 +1,21 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "net.deechael.concentration.fabric.mixin", + "refmap": "${mod_id}.refmap.json", + "compatibilityLevel": "JAVA_21", + "mixins": [], + "plugin": "net.deechael.concentration.fabric.ConcentrationFabricMixinPlugin", + "client": [ + "SodiumVideoOptionsScreenMixin", + "WindowMixin", + "VulkanWindowMixin", + "OptionsMixin", + "GLFWMixin" + ], + "server": [], + "injectors": { + "defaultRequire": 1 + } +} + diff --git a/fabric/src/main/resources/fabric.mod.json b/fabric/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..38f3d52 --- /dev/null +++ b/fabric/src/main/resources/fabric.mod.json @@ -0,0 +1,38 @@ +{ + "schemaVersion": 1, + "id": "${mod_id}", + "version": "${version}", + "name": "${mod_name}", + "description": "${description}", + "authors": [ + "${mod_author}" + ], + "contact": { + "sources": "https://github.com/DeeChael/Concentration" + }, + "license": "${license}", + "icon": "logo.png", + "environment": "client", + "entrypoints": { + "client": [ + "net.deechael.concentration.fabric.ConcentrationFabric" + ], + "modmenu" : [ + "net.deechael.concentration.fabric.compat.ModMenuCompat" + ] + }, + "mixins": [ + "${mod_id}.mixins.json", + "${mod_id}.fabric.mixins.json" + ], + "depends": { + "fabricloader": ">=${fabric_loader_version}", + "fabric-api": "*", + "minecraft": "${minecraft_version}", + "java": ">=${java_version}" + }, + "suggests": { + "sodium": ">=0.5.9" + } +} + \ No newline at end of file diff --git a/forge/build.gradle b/forge/build.gradle new file mode 100644 index 0000000..712a2f9 --- /dev/null +++ b/forge/build.gradle @@ -0,0 +1,89 @@ +plugins { + id 'multiloader-loader' + id 'net.minecraftforge.gradle' version '[6.0.24,6.2)' + id 'org.spongepowered.mixin' version '0.7-SNAPSHOT' +} +base { + archivesName = "${mod_name}-forge-${minecraft_version}" +} +mixin { + config("${mod_id}.mixins.json") + config("${mod_id}.forge.mixins.json") +} + +minecraft { + mappings channel: 'official', version: minecraft_version + + copyIdeResources = true //Calls processResources when in dev + + reobf = false // Forge 1.20.6+ uses official mappings at runtime, so we shouldn't reobf from official to SRG + + // Automatically enable forge AccessTransformers if the file exists + // This location is hardcoded in Forge and can not be changed. + // https://github.com/MinecraftForge/MinecraftForge/blob/be1698bb1554f9c8fa2f58e32b9ab70bc4385e60/fmlloader/src/main/java/net/minecraftforge/fml/loading/moddiscovery/ModFile.java#L123 + // Forge still uses SRG names during compile time, so we cannot use the common AT's + def at = file('src/main/resources/META-INF/accesstransformer.cfg') + if (at.exists()) { + accessTransformer = at + } + + runs { + client { + workingDirectory file('runs/client') + ideaModule "${rootProject.name}.${project.name}.main" + taskName 'Client' + mods { + modClientRun { + source sourceSets.main + } + } + } + + server { + workingDirectory file('runs/server') + ideaModule "${rootProject.name}.${project.name}.main" + taskName 'Server' + mods { + modServerRun { + source sourceSets.main + } + } + } + + data { + workingDirectory file('runs/data') + ideaModule "${rootProject.name}.${project.name}.main" + args '--mod', mod_id, '--all', '--output', file('src/generated/resources/'), '--existing', file('src/main/resources/') + taskName 'Data' + mods { + modDataRun { + source sourceSets.main + } + } + } + } +} + +sourceSets.main.resources.srcDir 'src/generated/resources' + +dependencies { + minecraft "net.minecraftforge:forge:${minecraft_version}-${forge_version}" + annotationProcessor("org.spongepowered:mixin:0.8.5-SNAPSHOT:processor") + + // Forge's hack fix + implementation('net.sf.jopt-simple:jopt-simple:5.0.4') { version { strictly '5.0.4' } } +} + +publishing { + publications { + mavenJava(MavenPublication) { + fg.component(it) + } + } +} + +sourceSets.each { + def dir = layout.buildDirectory.dir("sourcesSets/$it.name") + it.output.resourcesDir = dir + it.java.destinationDirectory = dir +} \ No newline at end of file diff --git a/forge/src/main/java/net/deechael/concentration/forge/ConcentrationForge.java b/forge/src/main/java/net/deechael/concentration/forge/ConcentrationForge.java new file mode 100644 index 0000000..361c439 --- /dev/null +++ b/forge/src/main/java/net/deechael/concentration/forge/ConcentrationForge.java @@ -0,0 +1,61 @@ +package net.deechael.concentration.forge; + +import net.deechael.concentration.Concentration; +import net.deechael.concentration.ConcentrationConstants; +import net.deechael.concentration.config.ConcentrationConfigScreen; +import net.deechael.concentration.forge.config.ConcentrationConfigForge; +import net.minecraft.client.OptionInstance; +import net.minecraft.network.chat.Component; +import net.minecraftforge.client.ConfigScreenHandler; +import net.minecraftforge.eventbus.api.IEventBus; +import net.minecraftforge.fml.ModContainer; +import net.minecraftforge.fml.ModLoadingContext; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.loading.FMLEnvironment; + +/** + * Mod entrance for Forge of Concentration + * + * @author DeeChael + */ +@Mod(value = ConcentrationConstants.MOD_ID) +public class ConcentrationForge { + + public ConcentrationForge(ModContainer container, IEventBus eventBus) { + Concentration.init(); + + if (FMLEnvironment.dist.isClient()) { + ModLoadingContext.get().registerExtensionPoint(ConfigScreenHandler.ConfigScreenFactory.class, () -> new ConfigScreenHandler.ConfigScreenFactory((parent) -> new ConcentrationConfigScreen(Component.literal(ConcentrationConstants.MOD_NAME), parent) { + @Override + public void save() { + ConcentrationConfigForge.SPECS.save(); + Concentration.toggleFullScreenMode(minecraft.options, minecraft.options.fullscreen().get()); + } + + @Override + public void addElements() { + addOption(OptionInstance.createBoolean("concentration.config.customization.enabled", + ConcentrationConfigForge.CUSTOMIZED.get(), + ConcentrationConfigForge.CUSTOMIZED::set)); + addOption(OptionInstance.createBoolean("concentration.config.customization.related", + ConcentrationConfigForge.RELATED.get(), + ConcentrationConfigForge.RELATED::set)); + + addIntField(Component.translatable("concentration.config.customization.x"), + ConcentrationConfigForge.X, + ConcentrationConfigForge.X::set); + addIntField(Component.translatable("concentration.config.customization.y"), + ConcentrationConfigForge.Y, + ConcentrationConfigForge.Y::set); + addIntField(Component.translatable("concentration.config.customization.width"), + ConcentrationConfigForge.WIDTH, + ConcentrationConfigForge.WIDTH::set); + addIntField(Component.translatable("concentration.config.customization.height"), + ConcentrationConfigForge.HEIGHT, + ConcentrationConfigForge.HEIGHT::set); + } + })); + } + } + +} \ No newline at end of file diff --git a/forge/src/main/java/net/deechael/concentration/forge/config/ConcentrationConfigForge.java b/forge/src/main/java/net/deechael/concentration/forge/config/ConcentrationConfigForge.java new file mode 100644 index 0000000..12c328b --- /dev/null +++ b/forge/src/main/java/net/deechael/concentration/forge/config/ConcentrationConfigForge.java @@ -0,0 +1,89 @@ +package net.deechael.concentration.forge.config; + +import com.electronwill.nightconfig.core.file.CommentedFileConfig; +import com.electronwill.nightconfig.core.io.WritingMode; +import net.deechael.concentration.ConcentrationConstants; +import net.deechael.concentration.FullscreenMode; +import net.deechael.concentration.config.Config; +import net.minecraftforge.common.ForgeConfigSpec; +import net.minecraftforge.fml.loading.FMLPaths; + +import java.nio.file.Path; + +public final class ConcentrationConfigForge implements Config { + + public final static ConcentrationConfigForge INSTANCE = new ConcentrationConfigForge(); + + public static final ForgeConfigSpec SPECS; + public static final ForgeConfigSpec.BooleanValue CUSTOMIZED; + public static final ForgeConfigSpec.BooleanValue RELATED; + public static final ForgeConfigSpec.IntValue X; + public static final ForgeConfigSpec.IntValue Y; + public static final ForgeConfigSpec.IntValue WIDTH; + public static final ForgeConfigSpec.IntValue HEIGHT; + public static final ForgeConfigSpec.EnumValue FULLSCREEN; + + private static boolean loaded = false; + + static { + ForgeConfigSpec.Builder builder = new ForgeConfigSpec.Builder(); + + builder.push("concentration"); + + CUSTOMIZED = builder.comment("Whether the window size and pos is customized") + .define("customized", false); + RELATED = builder.comment("Whether the window pos should related to the monitor") + .define("related", false); + + X = builder.comment("X coordinate") + .defineInRange("x", 0, Integer.MIN_VALUE, Integer.MAX_VALUE); + Y = builder.comment("Y coordinate") + .defineInRange("y", 0, Integer.MIN_VALUE, Integer.MAX_VALUE); + WIDTH = builder.comment("Width") + .defineInRange("width", 800, 1, Integer.MAX_VALUE); + HEIGHT = builder.comment("Height") + .defineInRange("height", 600, 1, Integer.MAX_VALUE); + FULLSCREEN = builder.comment("Fullscreen mode") + .defineEnum("fullscreen", FullscreenMode.BORDERLESS); + + builder.pop(); + + SPECS = builder.build(); + } + + public static ConcentrationConfigForge ensureLoaded() { + if (!loaded) { + ConcentrationConstants.LOGGER.info("Loading Concentration Config"); + + Path path = FMLPaths.CONFIGDIR.get().resolve("concentration-client.toml"); + CommentedFileConfig config = CommentedFileConfig.builder(path) + .sync() + .autosave() + .writingMode(WritingMode.REPLACE) + .build(); + config.load(); + + loaded = true; + } + return INSTANCE; + } + + private ConcentrationConfigForge() { + } + + @Override + public FullscreenMode getFullscreenMode() { + return FULLSCREEN.get(); + } + + @Override + public void setFullscreenMode(FullscreenMode fullscreenMode) { + FULLSCREEN.set(fullscreenMode); + } + + @Override + public void save() { + SPECS.save(); + } + +} diff --git a/forge/src/main/java/net/deechael/concentration/forge/config/ForgeConfigProvider.java b/forge/src/main/java/net/deechael/concentration/forge/config/ForgeConfigProvider.java new file mode 100644 index 0000000..7aac8b0 --- /dev/null +++ b/forge/src/main/java/net/deechael/concentration/forge/config/ForgeConfigProvider.java @@ -0,0 +1,13 @@ +package net.deechael.concentration.forge.config; + +import net.deechael.concentration.config.Config; +import net.deechael.concentration.config.ConfigProvider; + +public class ForgeConfigProvider implements ConfigProvider { + + @Override + public Config ensureLoaded() { + return ConcentrationConfigForge.ensureLoaded(); + } + +} diff --git a/forge/src/main/java/net/deechael/concentration/forge/mixin/WindowMixin.java b/forge/src/main/java/net/deechael/concentration/forge/mixin/WindowMixin.java new file mode 100644 index 0000000..2c00c32 --- /dev/null +++ b/forge/src/main/java/net/deechael/concentration/forge/mixin/WindowMixin.java @@ -0,0 +1,216 @@ +package net.deechael.concentration.forge.mixin; + +import com.mojang.blaze3d.platform.Monitor; +import com.mojang.blaze3d.platform.ScreenManager; +import com.mojang.blaze3d.platform.VideoMode; +import com.mojang.blaze3d.platform.Window; +import net.deechael.concentration.ConcentrationConstants; +import net.deechael.concentration.FullscreenMode; +import net.deechael.concentration.forge.config.ConcentrationConfigForge; +import org.lwjgl.glfw.GLFW; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** + * Borderless implementation + * + * @author DeeChael + */ +@Mixin(Window.class) +public abstract class WindowMixin { + + @Shadow + private boolean fullscreen; + + @Shadow + @Final + private ScreenManager screenManager; + + @Shadow + private int x; + @Shadow + private int y; + @Shadow + private int width; + @Shadow + private int height; + + @Unique + private long concentration$lastMonitor = -1; + + @Unique + private boolean concentration$cachedSize = false; + @Unique + private boolean concentration$cachedPos = false; + @Unique + private boolean concentration$cacheSizeLock = false; + + @Unique + private int concentration$cachedX = 0; + @Unique + private int concentration$cachedY = 0; + @Unique + private int concentration$cachedWidth = 0; + @Unique + private int concentration$cachedHeight = 0; + + @Inject(method = "onMove", at = @At("HEAD")) + private void inject$onMove$head(long window, int x, int y, CallbackInfo ci) { + if (!this.fullscreen) { + if (!this.concentration$cachedPos) { + ConcentrationConstants.LOGGER.info("Window position has been cached"); + } + this.concentration$cachedPos = true; + this.concentration$cachedX = x; + this.concentration$cachedY = y; + } + } + + @Inject(method = "onResize", at = @At("HEAD")) + private void inject$onResize$head(long window, int width, int height, CallbackInfo ci) { + if (!this.fullscreen && !this.concentration$cacheSizeLock) { + if (!this.concentration$cachedSize) { + ConcentrationConstants.LOGGER.info("Window size has been cached"); + } + this.concentration$cachedSize = true; + this.concentration$cachedWidth = width; + this.concentration$cachedHeight = height; + } + } + + @Redirect(method = "setMode", at = @At(value = "INVOKE", remap = false, target = "Lorg/lwjgl/glfw/GLFW;glfwSetWindowMonitor(JJIIIII)V")) + private void redirect$glfwSetWindowMonitor(long window, long monitor, int xpos, int ypos, int width, int height, int refreshRate) { + ConcentrationConstants.LOGGER.info("================= [Concentration Start] ================="); + ConcentrationConstants.LOGGER.info("Trying to modify window monitor"); + + // Monitor is 0 means the game is windowed mode, so the expression means if is toggling to fullscreen + if (monitor != 0L) { + ConcentrationConstants.LOGGER.info("Modifying window size limits"); + GLFW.glfwSetWindowSizeLimits(window, 0, 0, width, height); + } + + // Because whether in fullscreen mode or windowed mode + // The final step is same + // So I extracted the value then execute the final step + int finalWidth; + int finalHeight; + + int finalX; + int finalY; + + if (this.fullscreen) { + ConcentrationConfigForge.ensureLoaded(); + + // If the game started with fullscreen mode, when switching to windowed mode, it will be forced to move to the primary monitor + // Though size and position isn't be set at initialization, but I think the window should be at the initial monitor + // So save the monitor and use the monitor value when the size isn't cached + this.concentration$lastMonitor = monitor; + + // Lock caching, because when switching back, the window will be once resized to the maximum value and the cache value will be wrong + // Position won't be affected, so it doesn't need lock + this.concentration$cacheSizeLock = true; + ConcentrationConstants.LOGGER.info("Locked size caching"); + + if (ConcentrationConfigForge.FULLSCREEN.get() == FullscreenMode.NATIVE) { + ConcentrationConstants.LOGGER.info("Fullscreen mode is native, apply now!"); + GLFW.glfwSetWindowMonitor(window, monitor, xpos, ypos, width, height, -1); + ConcentrationConstants.LOGGER.info("================= [Concentration End] ================="); + return; + } + + ConcentrationConstants.LOGGER.info("Trying to switch to borderless fullscreen mode"); + + // Get the monitor the user want to use and get the relative position in the system + // The monitor is always non-null because when switching fullscreen mode, there must be a monitor to put the window + Monitor monitorInstance = this.screenManager.getMonitor(monitor); + ConcentrationConstants.LOGGER.info("Current fullscreen monitor is {}", monitor); + + // Remove the title bar to prevent that user can see the title bar if they put their monitors vertically connected + GLFW.glfwSetWindowAttrib(window, GLFW.GLFW_DECORATED, GLFW.GLFW_FALSE); + ConcentrationConstants.LOGGER.info("Trying to remove the title bar"); + + if (ConcentrationConfigForge.CUSTOMIZED.get()) { + final boolean related = ConcentrationConfigForge.RELATED.get(); + final int configX = ConcentrationConfigForge.X.get(); + final int configY = ConcentrationConfigForge.Y.get(); + final int configWidth = ConcentrationConfigForge.WIDTH.get(); + final int configHeight = ConcentrationConfigForge.HEIGHT.get(); + + ConcentrationConstants.LOGGER.info("Customization enabled, so replace the fullscreen size with customized size"); + + finalX = configX + (related ? monitorInstance.getX() : 0); + finalY = configY - (configHeight == height ? 1 : 0) + (related ? monitorInstance.getY() : 0); + finalWidth = configWidth; + finalHeight = configHeight + (configHeight == height ? 1 : 0); + } else { + // If we make the window not decorated and set the window size exactly the same with the screen size, it will become native fullscreen mode + // to prevent this, I enlarge the height by 1 pixel and move up the window by 1 pixel which won't affect anything (unless you have a screen + // which is added above the monitor which holds the game) and will have a good experience + // Actually this is a little bit dirty, needs to find a better way to solve it + finalX = monitorInstance.getX(); + finalY = monitorInstance.getY() - 1; + finalWidth = width; + finalHeight = height + 1; + } + + this.x = finalX; + this.y = finalY; + this.width = finalWidth; + this.height = finalHeight; + } else { + ConcentrationConstants.LOGGER.info("Trying to switch to windowed mode"); + + // Re-add the title bar so user can move the window and minimize, maximize and close the window + ConcentrationConstants.LOGGER.info("Trying to add title bar back"); + GLFW.glfwSetWindowAttrib(window, GLFW.GLFW_DECORATED, GLFW.GLFW_TRUE); + + ConcentrationConstants.LOGGER.info("Trying to use cached value to resize the window"); + + // Make sure that Concentration has cached position and size, because position size won't be cached when the game starting in fullscreen mode + finalWidth = concentration$cachedSize ? concentration$cachedWidth : width; + finalHeight = concentration$cachedSize ? concentration$cachedHeight : height; + + // To make sure that even starting with fullscreen mode can also make the window stay at the current monitor + // So I set two ways to ensure the position + if (this.concentration$cachedPos) { + // If Concentration cached the pos, use the cached value + finalX = concentration$cachedX; + finalY = concentration$cachedY; + } else if (this.concentration$lastMonitor != -1) { + // or else maybe the game started with fullscreen mode, so I don't need to care about the size + // only need to make sure that the position is in the correct monitor + Monitor monitorInstance = this.screenManager.getMonitor(this.concentration$lastMonitor); + VideoMode videoMode = monitorInstance.getCurrentMode(); + finalX = (videoMode.getWidth() - finalWidth) / 2; + finalY = (videoMode.getHeight() - finalHeight) / 2; + } else { + // if both value are missed, use the default value to prevent errors + finalX = xpos; + finalY = ypos; + } + + // Unlock caching, because user can change the window size now + this.concentration$cacheSizeLock = false; + ConcentrationConstants.LOGGER.info("Unlocked size caching"); + } + + ConcentrationConstants.LOGGER.info("Window size: {}, {}, position: {}, {}", finalWidth, finalHeight, finalX, finalY); + + ConcentrationConstants.LOGGER.info("Trying to resize and reposition the window"); + GLFW.glfwSetWindowMonitor(window, 0L, finalX, finalY, finalWidth, finalHeight, -1); + + ConcentrationConstants.LOGGER.info("================= [Concentration End] ================="); + } + + @Redirect(method = "setMode", at = @At(value = "INVOKE", remap = false, target = "Lorg/lwjgl/glfw/GLFW;glfwGetWindowMonitor(J)J")) + private long redirect$glfwGetWindowMonitor(long window) { + return 1L; + } + +} \ No newline at end of file diff --git a/forge/src/main/resources/META-INF/mods.toml b/forge/src/main/resources/META-INF/mods.toml new file mode 100644 index 0000000..0f5b062 --- /dev/null +++ b/forge/src/main/resources/META-INF/mods.toml @@ -0,0 +1,26 @@ +modLoader = "javafml" +loaderVersion = "${forge_loader_version_range}" +license = "${license}" + +[[mods]] +modId = "${mod_id}" +version = "${version}" +displayName = "${mod_name}" +logoFile = "${mod_id}.png" +credits = "${credits}" +authors = "${mod_author}" +description = '''${description}''' + +[[dependencies.${mod_id}]] +modId = "forge" +mandatory = true +versionRange = "[${forge_version},)" +ordering = "NONE" +side = "CLIENT" + +[[dependencies.${mod_id}]] +modId = "minecraft" +mandatory = true +versionRange = "${minecraft_version_range}" +ordering = "NONE" +side = "CLIENT" \ No newline at end of file diff --git a/forge/src/main/resources/META-INF/services/net.deechael.concentration.config.ConfigProvider b/forge/src/main/resources/META-INF/services/net.deechael.concentration.config.ConfigProvider new file mode 100644 index 0000000..e51055e --- /dev/null +++ b/forge/src/main/resources/META-INF/services/net.deechael.concentration.config.ConfigProvider @@ -0,0 +1 @@ +net.deechael.concentration.forge.config.ForgeConfigProvider \ No newline at end of file diff --git a/forge/src/main/resources/concentration.forge.mixins.json b/forge/src/main/resources/concentration.forge.mixins.json new file mode 100644 index 0000000..edd715b --- /dev/null +++ b/forge/src/main/resources/concentration.forge.mixins.json @@ -0,0 +1,14 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "net.deechael.concentration.forge.mixin", + "compatibilityLevel": "JAVA_21", + "mixins": [], + "client": [ + "WindowMixin" + ], + "server": [], + "injectors": { + "defaultRequire": 1 + } +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..1b2a003 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,27 @@ +version=2.0.0 +group=net.deechael.concentration +java_version=21 + +minecraft_version=1.21 +mod_name=Concentration +mod_author=DeeChael +mod_id=concentration +license=MIT +credits= +description=Add borderless fullscreen support for minecraft +minecraft_version_range=[1.21.1, 1.21.2) +neo_form_version=1.21-20240613.152323 +parchment_minecraft=1.21 +parchment_version=2024.06.23 + +fabric_version=0.100.1+1.21 +fabric_loader_version=0.15.11 + +forge_version=51.0.17 +forge_loader_version_range=[51,) + +neoforge_version=21.0.37-beta +neoforge_loader_version_range=[4,) + +org.gradle.jvmargs=-Xmx3G +org.gradle.daemon=false diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e644113 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..a441313 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..b740cf1 --- /dev/null +++ b/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..25da30d --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/neoforge/build.gradle b/neoforge/build.gradle new file mode 100644 index 0000000..531f06d --- /dev/null +++ b/neoforge/build.gradle @@ -0,0 +1,50 @@ +plugins { + id 'multiloader-loader' + id 'net.neoforged.moddev' +} + +neoForge { + version = neoforge_version + // Automatically enable neoforge AccessTransformers if the file exists + def at = project(':common').file('src/main/resources/META-INF/accesstransformer.cfg') + if (at.exists()) { + accessTransformers.add(at.absolutePath) + } + parchment { + minecraftVersion = parchment_minecraft + mappingsVersion = parchment_version + } + runs { + configureEach { + systemProperty('neoforge.enabledGameTestNamespaces', mod_id) + ideName = "NeoForge ${it.name.capitalize()} (${project.path})" // Unify the run config names with fabric + } + client { + client() + } + data { + data() + } + server { + server() + } + } + mods { + "${mod_id}" { + sourceSet sourceSets.main + } + } +} + +sourceSets.main.resources { srcDir 'src/generated/resources' } + +repositories { + maven { + url = "https://api.modrinth.com/maven" + } +} + +dependencies { + implementation("org.embeddedt:embeddium-1.21:1.0.8-beta.348+mc1.21") + implementation "maven.modrinth:sodium:mc1.21-0.6.0-beta.2-neoforge" +} \ No newline at end of file diff --git a/neoforge/src/main/java/net/deechael/concentration/neoforge/ConcentrationNeoForge.java b/neoforge/src/main/java/net/deechael/concentration/neoforge/ConcentrationNeoForge.java new file mode 100644 index 0000000..b59107d --- /dev/null +++ b/neoforge/src/main/java/net/deechael/concentration/neoforge/ConcentrationNeoForge.java @@ -0,0 +1,80 @@ +package net.deechael.concentration.neoforge; + +import net.deechael.concentration.Concentration; +import net.deechael.concentration.ConcentrationConstants; +import net.deechael.concentration.config.ConcentrationConfigScreen; +import net.deechael.concentration.neoforge.compat.EmbeddiumCompat; +import net.deechael.concentration.neoforge.config.ConcentrationConfigNeoForge; +import net.minecraft.client.Minecraft; +import net.minecraft.client.OptionInstance; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.fml.ModContainer; +import net.neoforged.fml.ModList; +import net.neoforged.fml.ModLoadingContext; +import net.neoforged.fml.common.Mod; +import net.neoforged.fml.config.ModConfig; +import net.neoforged.fml.loading.FMLEnvironment; +import net.neoforged.neoforge.client.gui.IConfigScreenFactory; +import org.jetbrains.annotations.NotNull; + +/** + * Mod entrance for NeoForge of Concentration + * + * @author DeeChael + */ +@Mod(value = ConcentrationConstants.MOD_ID, dist = Dist.CLIENT) +public class ConcentrationNeoForge { + + public ConcentrationNeoForge(ModContainer container, IEventBus eventBus) { + Concentration.init(); + + container.registerConfig(ModConfig.Type.CLIENT, ConcentrationConfigNeoForge.SPECS, "concentration-client.toml"); + + if (ModList.get().isLoaded("embeddium")) { + EmbeddiumCompat.init(); + } + + if (FMLEnvironment.dist.isClient()) { + ModLoadingContext.get().registerExtensionPoint(IConfigScreenFactory.class, () -> new IConfigScreenFactory() { + @Override + public @NotNull Screen createScreen(@NotNull ModContainer modContainer, @NotNull Screen parent) { + return new ConcentrationConfigScreen(Component.literal(ConcentrationConstants.MOD_NAME), parent) { + + @Override + public void save() { + ConcentrationConfigNeoForge.SPECS.save(); + Concentration.toggleFullScreenMode(minecraft.options, minecraft.options.fullscreen().get()); + } + + @Override + public void addElements() { + addOption(OptionInstance.createBoolean("concentration.config.customization.enabled", + ConcentrationConfigNeoForge.CUSTOMIZED.get(), + ConcentrationConfigNeoForge.CUSTOMIZED::set)); + addOption(OptionInstance.createBoolean("concentration.config.customization.related", + ConcentrationConfigNeoForge.RELATED.get(), + ConcentrationConfigNeoForge.RELATED::set)); + + addIntField(Component.translatable("concentration.config.customization.x"), + ConcentrationConfigNeoForge.X, + ConcentrationConfigNeoForge.X::set); + addIntField(Component.translatable("concentration.config.customization.y"), + ConcentrationConfigNeoForge.Y, + ConcentrationConfigNeoForge.Y::set); + addIntField(Component.translatable("concentration.config.customization.width"), + ConcentrationConfigNeoForge.WIDTH, + ConcentrationConfigNeoForge.WIDTH::set); + addIntField(Component.translatable("concentration.config.customization.height"), + ConcentrationConfigNeoForge.HEIGHT, + ConcentrationConfigNeoForge.HEIGHT::set); + } + }; + } + }); + } + } + +} \ No newline at end of file diff --git a/neoforge/src/main/java/net/deechael/concentration/neoforge/ConcentrationNeoForgeMixinPlugin.java b/neoforge/src/main/java/net/deechael/concentration/neoforge/ConcentrationNeoForgeMixinPlugin.java new file mode 100644 index 0000000..acd2a82 --- /dev/null +++ b/neoforge/src/main/java/net/deechael/concentration/neoforge/ConcentrationNeoForgeMixinPlugin.java @@ -0,0 +1,55 @@ +package net.deechael.concentration.neoforge; + +import net.neoforged.fml.ModList; +import org.objectweb.asm.tree.ClassNode; +import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin; +import org.spongepowered.asm.mixin.extensibility.IMixinInfo; + +import java.util.List; +import java.util.Set; + +/** + * Mixin plugin to make sure that sodium support only turned on when sodium installed + * + * @author DeeChael + */ +public class ConcentrationNeoForgeMixinPlugin implements IMixinConfigPlugin { + + @Override + public void onLoad(String mixinPackage) { + } + + @Override + public String getRefMapperConfig() { + return null; + } + + @Override + public boolean shouldApplyMixin(String targetClassName, String mixinClassName) { + return mixinClassName.equals("net.deechael.concentration.fabric.mixin.WindowMixin") + || (!ModList.get().isLoaded("embeddium") && checkSodium(mixinClassName)); + } + + @Override + public void acceptTargets(Set myTargets, Set otherTargets) { + } + + @Override + public List getMixins() { + return null; + } + + @Override + public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { + } + + @Override + public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { + } + + private static boolean checkSodium(String mixinClassName) { + return "net.deechael.concentration.neoforge.mixin.SodiumVideoOptionsScreenMixin".equals(mixinClassName) + && ModList.get().isLoaded("sodium"); + } + +} diff --git a/neoforge/src/main/java/net/deechael/concentration/neoforge/compat/EmbeddiumCompat.java b/neoforge/src/main/java/net/deechael/concentration/neoforge/compat/EmbeddiumCompat.java new file mode 100644 index 0000000..9d6f268 --- /dev/null +++ b/neoforge/src/main/java/net/deechael/concentration/neoforge/compat/EmbeddiumCompat.java @@ -0,0 +1,64 @@ +package net.deechael.concentration.neoforge.compat; + +import net.deechael.concentration.Concentration; +import net.deechael.concentration.ConcentrationConstants; +import net.deechael.concentration.FullscreenMode; +import net.deechael.concentration.neoforge.config.ConcentrationConfigNeoForge; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import org.embeddedt.embeddium.api.OptionGroupConstructionEvent; +import org.embeddedt.embeddium.api.options.control.CyclingControl; +import org.embeddedt.embeddium.api.options.control.TickBoxControl; +import org.embeddedt.embeddium.api.options.storage.MinecraftOptionsStorage; +import org.embeddedt.embeddium.api.options.structure.OptionImpl; +import org.embeddedt.embeddium.api.options.structure.StandardOptions; + +/** + * Make Embedddium fullscreen option follow Concentration function + * + * @author DeeChael + */ +public class EmbeddiumCompat { + + public static void init() { + OptionGroupConstructionEvent.BUS.addListener(event -> { + if (event.getId() != null && event.getId().toString().equals(StandardOptions.Group.WINDOW.toString())) { + var options = event.getOptions(); + for (int i = 0; i < options.size(); i++) { + if (options.get(i).getId().toString().equals(StandardOptions.Option.FULLSCREEN.toString())) { + options.add(i, OptionImpl.createBuilder(FullscreenMode.class, MinecraftOptionsStorage.INSTANCE) + .setId(ResourceLocation.fromNamespaceAndPath(ConcentrationConstants.MOD_ID, "fullscreen_mode")) + .setName(Component.translatable("concentration.option.fullscreen_mode")) + .setTooltip(Component.translatable("concentration.option.fullscreen_mode.tooltip")) + .setControl((opt) -> new CyclingControl<>(opt, FullscreenMode.class, new Component[]{ + Component.translatable("concentration.option.fullscreen_mode.borderless"), + Component.translatable("concentration.option.fullscreen_mode.native") + })) + .setBinding((vanillaOpts, value) -> { + ConcentrationConfigNeoForge.ensureLoaded().setFullscreenMode(value); + ConcentrationConfigNeoForge.ensureLoaded().save(); + if (vanillaOpts.fullscreen().get()) { + // If fullscreen turns on, re-toggle to changing the fullscreen mode instantly + Concentration.toggleFullScreenMode(vanillaOpts, true); + } + }, + (vanillaOpts) -> ConcentrationConfigNeoForge.ensureLoaded().getFullscreenMode() + ) + .build()); + options.set( + i + 1, + OptionImpl.createBuilder(Boolean.TYPE, MinecraftOptionsStorage.INSTANCE) + .setId(StandardOptions.Option.FULLSCREEN) + .setName(Component.translatable("options.fullscreen")) + .setTooltip(Component.translatable("sodium.options.fullscreen.tooltip")) + .setControl(TickBoxControl::new) + .setBinding(Concentration::toggleFullScreenMode, (opts) -> opts.fullscreen().get()).build() + ); + break; + } + } + } + }); + } + +} diff --git a/neoforge/src/main/java/net/deechael/concentration/neoforge/config/ConcentrationConfigNeoForge.java b/neoforge/src/main/java/net/deechael/concentration/neoforge/config/ConcentrationConfigNeoForge.java new file mode 100644 index 0000000..ed54da0 --- /dev/null +++ b/neoforge/src/main/java/net/deechael/concentration/neoforge/config/ConcentrationConfigNeoForge.java @@ -0,0 +1,89 @@ +package net.deechael.concentration.neoforge.config; + +import com.electronwill.nightconfig.core.file.CommentedFileConfig; +import com.electronwill.nightconfig.core.io.WritingMode; +import net.deechael.concentration.ConcentrationConstants; +import net.deechael.concentration.FullscreenMode; +import net.deechael.concentration.config.Config; +import net.neoforged.fml.loading.FMLPaths; +import net.neoforged.neoforge.common.ModConfigSpec; + +import java.nio.file.Path; + +public final class ConcentrationConfigNeoForge implements Config { + + public final static ConcentrationConfigNeoForge INSTANCE = new ConcentrationConfigNeoForge(); + + public static final ModConfigSpec SPECS; + public static final ModConfigSpec.BooleanValue CUSTOMIZED; + public static final ModConfigSpec.BooleanValue RELATED; + public static final ModConfigSpec.IntValue X; + public static final ModConfigSpec.IntValue Y; + public static final ModConfigSpec.IntValue WIDTH; + public static final ModConfigSpec.IntValue HEIGHT; + public static final ModConfigSpec.EnumValue FULLSCREEN; + + // private static boolean loaded = false; + + static { + ModConfigSpec.Builder builder = new ModConfigSpec.Builder(); + + builder.push("concentration"); + + CUSTOMIZED = builder.comment("Whether the window size and pos is customized") + .define("customized", false); + RELATED = builder.comment("Whether the window pos should related to the monitor") + .define("related", false); + + X = builder.comment("X coordinate") + .defineInRange("x", 0, Integer.MIN_VALUE, Integer.MAX_VALUE); + Y = builder.comment("Y coordinate") + .defineInRange("y", 0, Integer.MIN_VALUE, Integer.MAX_VALUE); + WIDTH = builder.comment("Width") + .defineInRange("width", 800, 1, Integer.MAX_VALUE); + HEIGHT = builder.comment("Height") + .defineInRange("height", 600, 1, Integer.MAX_VALUE); + FULLSCREEN = builder.comment("Fullscreen mode") + .defineEnum("fullscreen", FullscreenMode.BORDERLESS); + + builder.pop(); + + SPECS = builder.build(); + } + + public static ConcentrationConfigNeoForge ensureLoaded() {/* + if (!loaded) { + ConcentrationConstants.LOGGER.info("Loading Concentration Config"); + + Path path = FMLPaths.CONFIGDIR.get().resolve("concentration-client.toml"); + CommentedFileConfig config = CommentedFileConfig.builder(path) + .sync() + .autosave() + .writingMode(WritingMode.REPLACE) + .build(); + config.load(); + + loaded = true; + }*/ + return INSTANCE; + } + + private ConcentrationConfigNeoForge() { + } + + @Override + public FullscreenMode getFullscreenMode() { + return FULLSCREEN.get(); + } + + @Override + public void setFullscreenMode(FullscreenMode fullscreenMode) { + FULLSCREEN.set(fullscreenMode); + } + + @Override + public void save() { + SPECS.save(); + } + +} diff --git a/neoforge/src/main/java/net/deechael/concentration/neoforge/config/NeoForgeConfigProvider.java b/neoforge/src/main/java/net/deechael/concentration/neoforge/config/NeoForgeConfigProvider.java new file mode 100644 index 0000000..c84f302 --- /dev/null +++ b/neoforge/src/main/java/net/deechael/concentration/neoforge/config/NeoForgeConfigProvider.java @@ -0,0 +1,13 @@ +package net.deechael.concentration.neoforge.config; + +import net.deechael.concentration.config.Config; +import net.deechael.concentration.config.ConfigProvider; + +public class NeoForgeConfigProvider implements ConfigProvider { + + @Override + public Config ensureLoaded() { + return ConcentrationConfigNeoForge.ensureLoaded(); + } + +} diff --git a/neoforge/src/main/java/net/deechael/concentration/neoforge/mixin/SodiumVideoOptionsScreenMixin.java b/neoforge/src/main/java/net/deechael/concentration/neoforge/mixin/SodiumVideoOptionsScreenMixin.java new file mode 100644 index 0000000..451f5ba --- /dev/null +++ b/neoforge/src/main/java/net/deechael/concentration/neoforge/mixin/SodiumVideoOptionsScreenMixin.java @@ -0,0 +1,84 @@ +package net.deechael.concentration.neoforge.mixin; + +import com.google.common.collect.ImmutableList; +import net.caffeinemc.mods.sodium.client.gui.SodiumGameOptionPages; +import net.caffeinemc.mods.sodium.client.gui.options.Option; +import net.caffeinemc.mods.sodium.client.gui.options.OptionGroup; +import net.caffeinemc.mods.sodium.client.gui.options.OptionImpl; +import net.caffeinemc.mods.sodium.client.gui.options.control.CyclingControl; +import net.caffeinemc.mods.sodium.client.gui.options.control.TickBoxControl; +import net.caffeinemc.mods.sodium.client.gui.options.storage.MinecraftOptionsStorage; +import net.deechael.concentration.Concentration; +import net.deechael.concentration.FullscreenMode; +import net.deechael.concentration.neoforge.config.ConcentrationConfigNeoForge; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.contents.TranslatableContents; +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.ModifyArg; + +import java.util.ArrayList; +import java.util.List; + +/** + * Hooks sodium options to make sure that changing fullscreen behaviour will use Concentration function instead of vanilla function + * + * @author DeeChael + */ +@Mixin(SodiumGameOptionPages.class) +public class SodiumVideoOptionsScreenMixin { + + @Shadow(remap = false) + @Final + private static MinecraftOptionsStorage vanillaOpts; + + @ModifyArg(method = "general", at = @At(value = "INVOKE", target = "Lnet/caffeinemc/mods/sodium/client/gui/options/OptionPage;(Lnet/minecraft/network/chat/Component;Lcom/google/common/collect/ImmutableList;)V"), index = 1) + private static ImmutableList inject$general(ImmutableList groups) { + List newGroups = new ArrayList<>(); + + for (OptionGroup group : groups) { + OptionGroup.Builder builder = OptionGroup.createBuilder(); + for (Option option : group.getOptions()) { + if (option.getName().getContents() instanceof TranslatableContents translatableContents) { + if (translatableContents.getKey().equals("options.fullscreen")) { + builder.add( + OptionImpl.createBuilder(FullscreenMode.class, vanillaOpts) + .setName(Component.translatable("concentration.option.fullscreen_mode")) + .setTooltip(Component.translatable("concentration.option.fullscreen_mode.tooltip")) + .setControl((opt) -> new CyclingControl<>(opt, FullscreenMode.class, new Component[]{ + Component.translatable("concentration.option.fullscreen_mode.borderless"), + Component.translatable("concentration.option.fullscreen_mode.native") + })) + .setBinding((options, value) -> { + ConcentrationConfigNeoForge.FULLSCREEN.set(value); + ConcentrationConfigNeoForge.SPECS.save(); + if (options.fullscreen().get()) { + // If fullscreen turns on, re-toggle to changing the fullscreen mode instantly + Concentration.toggleFullScreenMode(options, true); + } + }, + (options) -> ConcentrationConfigNeoForge.FULLSCREEN.get() + ) + .build() + ).add( + OptionImpl.createBuilder(boolean.class, vanillaOpts) + .setName(Component.translatable("options.fullscreen")) + .setTooltip(Component.translatable("sodium.options.fullscreen.tooltip")) + .setControl(TickBoxControl::new) + .setBinding(Concentration::toggleFullScreenMode, (options) -> options.fullscreen().get()) + .build() + ); + continue; + } + } + builder.add(option); + } + newGroups.add(builder.build()); + } + + return ImmutableList.copyOf(newGroups); + } + +} diff --git a/neoforge/src/main/java/net/deechael/concentration/neoforge/mixin/WindowMixin.java b/neoforge/src/main/java/net/deechael/concentration/neoforge/mixin/WindowMixin.java new file mode 100644 index 0000000..9126427 --- /dev/null +++ b/neoforge/src/main/java/net/deechael/concentration/neoforge/mixin/WindowMixin.java @@ -0,0 +1,216 @@ +package net.deechael.concentration.neoforge.mixin; + +import com.mojang.blaze3d.platform.Monitor; +import com.mojang.blaze3d.platform.ScreenManager; +import com.mojang.blaze3d.platform.VideoMode; +import com.mojang.blaze3d.platform.Window; +import net.deechael.concentration.ConcentrationConstants; +import net.deechael.concentration.FullscreenMode; +import net.deechael.concentration.neoforge.config.ConcentrationConfigNeoForge; +import org.lwjgl.glfw.GLFW; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** + * Borderless implementation + * + * @author DeeChael + */ +@Mixin(Window.class) +public abstract class WindowMixin { + + @Shadow + private boolean fullscreen; + + @Shadow + @Final + private ScreenManager screenManager; + + @Shadow + private int x; + @Shadow + private int y; + @Shadow + private int width; + @Shadow + private int height; + + @Unique + private long concentration$lastMonitor = -1; + + @Unique + private boolean concentration$cachedSize = false; + @Unique + private boolean concentration$cachedPos = false; + @Unique + private boolean concentration$cacheSizeLock = false; + + @Unique + private int concentration$cachedX = 0; + @Unique + private int concentration$cachedY = 0; + @Unique + private int concentration$cachedWidth = 0; + @Unique + private int concentration$cachedHeight = 0; + + @Inject(method = "onMove", at = @At("HEAD")) + private void inject$onMove$head(long window, int x, int y, CallbackInfo ci) { + if (!this.fullscreen) { + if (!this.concentration$cachedPos) { + ConcentrationConstants.LOGGER.info("Window position has been cached"); + } + this.concentration$cachedPos = true; + this.concentration$cachedX = x; + this.concentration$cachedY = y; + } + } + + @Inject(method = "onResize", at = @At("HEAD")) + private void inject$onResize$head(long window, int width, int height, CallbackInfo ci) { + if (!this.fullscreen && !this.concentration$cacheSizeLock) { + if (!this.concentration$cachedSize) { + ConcentrationConstants.LOGGER.info("Window size has been cached"); + } + this.concentration$cachedSize = true; + this.concentration$cachedWidth = width; + this.concentration$cachedHeight = height; + } + } + + @Redirect(method = "setMode", at = @At(value = "INVOKE", remap = false, target = "Lorg/lwjgl/glfw/GLFW;glfwSetWindowMonitor(JJIIIII)V")) + private void redirect$glfwSetWindowMonitor(long window, long monitor, int xpos, int ypos, int width, int height, int refreshRate) { + ConcentrationConstants.LOGGER.info("================= [Concentration Start] ================="); + ConcentrationConstants.LOGGER.info("Trying to modify window monitor"); + + // Monitor is 0 means the game is windowed mode, so the expression means if is toggling to fullscreen + if (monitor != 0L) { + ConcentrationConstants.LOGGER.info("Modifying window size limits"); + GLFW.glfwSetWindowSizeLimits(window, 0, 0, width, height); + } + + // Because whether in fullscreen mode or windowed mode + // The final step is same + // So I extracted the value then execute the final step + int finalWidth; + int finalHeight; + + int finalX; + int finalY; + + if (this.fullscreen) { + ConcentrationConfigNeoForge.ensureLoaded(); + + // If the game started with fullscreen mode, when switching to windowed mode, it will be forced to move to the primary monitor + // Though size and position isn't be set at initialization, but I think the window should be at the initial monitor + // So save the monitor and use the monitor value when the size isn't cached + this.concentration$lastMonitor = monitor; + + // Lock caching, because when switching back, the window will be once resized to the maximum value and the cache value will be wrong + // Position won't be affected, so it doesn't need lock + this.concentration$cacheSizeLock = true; + ConcentrationConstants.LOGGER.info("Locked size caching"); + + if (ConcentrationConfigNeoForge.FULLSCREEN.get() == FullscreenMode.NATIVE) { + ConcentrationConstants.LOGGER.info("Fullscreen mode is native, apply now!"); + GLFW.glfwSetWindowMonitor(window, monitor, xpos, ypos, width, height, -1); + ConcentrationConstants.LOGGER.info("================= [Concentration End] ================="); + return; + } + + ConcentrationConstants.LOGGER.info("Trying to switch to borderless fullscreen mode"); + + // Get the monitor the user want to use and get the relative position in the system + // The monitor is always non-null because when switching fullscreen mode, there must be a monitor to put the window + Monitor monitorInstance = this.screenManager.getMonitor(monitor); + ConcentrationConstants.LOGGER.info("Current fullscreen monitor is {}", monitor); + + // Remove the title bar to prevent that user can see the title bar if they put their monitors vertically connected + GLFW.glfwSetWindowAttrib(window, GLFW.GLFW_DECORATED, GLFW.GLFW_FALSE); + ConcentrationConstants.LOGGER.info("Trying to remove the title bar"); + + if (ConcentrationConfigNeoForge.CUSTOMIZED.get()) { + final boolean related = ConcentrationConfigNeoForge.RELATED.get(); + final int configX = ConcentrationConfigNeoForge.X.get(); + final int configY = ConcentrationConfigNeoForge.Y.get(); + final int configWidth = ConcentrationConfigNeoForge.WIDTH.get(); + final int configHeight = ConcentrationConfigNeoForge.HEIGHT.get(); + + ConcentrationConstants.LOGGER.info("Customization enabled, so replace the fullscreen size with customized size"); + + finalX = configX + (related ? monitorInstance.getX() : 0); + finalY = configY - (configHeight == height ? 1 : 0) + (related ? monitorInstance.getY() : 0); + finalWidth = configWidth; + finalHeight = configHeight + (configHeight == height ? 1 : 0); + } else { + // If we make the window not decorated and set the window size exactly the same with the screen size, it will become native fullscreen mode + // to prevent this, I enlarge the height by 1 pixel and move up the window by 1 pixel which won't affect anything (unless you have a screen + // which is added above the monitor which holds the game) and will have a good experience + // Actually this is a little bit dirty, needs to find a better way to solve it + finalX = monitorInstance.getX(); + finalY = monitorInstance.getY() - 1; + finalWidth = width; + finalHeight = height + 1; + } + + this.x = finalX; + this.y = finalY; + this.width = finalWidth; + this.height = finalHeight; + } else { + ConcentrationConstants.LOGGER.info("Trying to switch to windowed mode"); + + // Re-add the title bar so user can move the window and minimize, maximize and close the window + ConcentrationConstants.LOGGER.info("Trying to add title bar back"); + GLFW.glfwSetWindowAttrib(window, GLFW.GLFW_DECORATED, GLFW.GLFW_TRUE); + + ConcentrationConstants.LOGGER.info("Trying to use cached value to resize the window"); + + // Make sure that Concentration has cached position and size, because position size won't be cached when the game starting in fullscreen mode + finalWidth = concentration$cachedSize ? concentration$cachedWidth : width; + finalHeight = concentration$cachedSize ? concentration$cachedHeight : height; + + // To make sure that even starting with fullscreen mode can also make the window stay at the current monitor + // So I set two ways to ensure the position + if (this.concentration$cachedPos) { + // If Concentration cached the pos, use the cached value + finalX = concentration$cachedX; + finalY = concentration$cachedY; + } else if (this.concentration$lastMonitor != -1) { + // or else maybe the game started with fullscreen mode, so I don't need to care about the size + // only need to make sure that the position is in the correct monitor + Monitor monitorInstance = this.screenManager.getMonitor(this.concentration$lastMonitor); + VideoMode videoMode = monitorInstance.getCurrentMode(); + finalX = (videoMode.getWidth() - finalWidth) / 2; + finalY = (videoMode.getHeight() - finalHeight) / 2; + } else { + // if both value are missed, use the default value to prevent errors + finalX = xpos; + finalY = ypos; + } + + // Unlock caching, because user can change the window size now + this.concentration$cacheSizeLock = false; + ConcentrationConstants.LOGGER.info("Unlocked size caching"); + } + + ConcentrationConstants.LOGGER.info("Window size: {}, {}, position: {}, {}", finalWidth, finalHeight, finalX, finalY); + + ConcentrationConstants.LOGGER.info("Trying to resize and reposition the window"); + GLFW.glfwSetWindowMonitor(window, 0L, finalX, finalY, finalWidth, finalHeight, -1); + + ConcentrationConstants.LOGGER.info("================= [Concentration End] ================="); + } + + @Redirect(method = "setMode", at = @At(value = "INVOKE", remap = false, target = "Lorg/lwjgl/glfw/GLFW;glfwGetWindowMonitor(J)J")) + private long redirect$glfwGetWindowMonitor(long window) { + return 1L; + } + +} \ No newline at end of file diff --git a/neoforge/src/main/resources/META-INF/neoforge.mods.toml b/neoforge/src/main/resources/META-INF/neoforge.mods.toml new file mode 100644 index 0000000..4c83464 --- /dev/null +++ b/neoforge/src/main/resources/META-INF/neoforge.mods.toml @@ -0,0 +1,39 @@ +modLoader = "javafml" +loaderVersion = "${neoforge_loader_version_range}" +license = "${license}" + +[[mods]] +modId = "${mod_id}" +version = "${version}" +displayName = "${mod_name}" +logoFile = "logo.png" +credits = "${credits}" +authors = "${mod_author}" +description = "${description}" + +[[mixins]] +config = "${mod_id}.mixins.json" + +[[mixins]] +config = "${mod_id}.neoforge.mixins.json" + +[[dependencies.${ mod_id }]] +modId = "embeddium" +type = "optional" +versionRange = "[0,)" +ordering = "NONE" +side = "CLIENT" + +[[dependencies.${ mod_id }]] +modId = "neoforge" +type = "required" +versionRange = "${neoforge_loader_version_range}" +ordering = "NONE" +side = "BOTH" + +[[dependencies.${ mod_id }]] +modId = "minecraft" +type = "required" +versionRange = "${minecraft_version_range}" +ordering = "NONE" +side = "BOTH" \ No newline at end of file diff --git a/neoforge/src/main/resources/META-INF/services/net.deechael.concentration.config.ConfigProvider b/neoforge/src/main/resources/META-INF/services/net.deechael.concentration.config.ConfigProvider new file mode 100644 index 0000000..5fd7230 --- /dev/null +++ b/neoforge/src/main/resources/META-INF/services/net.deechael.concentration.config.ConfigProvider @@ -0,0 +1 @@ +net.deechael.concentration.neoforge.config.NeoForgeConfigProvider \ No newline at end of file diff --git a/neoforge/src/main/resources/concentration.neoforge.mixins.json b/neoforge/src/main/resources/concentration.neoforge.mixins.json new file mode 100644 index 0000000..bdb630b --- /dev/null +++ b/neoforge/src/main/resources/concentration.neoforge.mixins.json @@ -0,0 +1,14 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "net.deechael.concentration.neoforge.mixin", + "compatibilityLevel": "JAVA_21", + "mixins": [], + "client": [ + "WindowMixin" + ], + "server": [], + "injectors": { + "defaultRequire": 1 + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..fc6fd62 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,51 @@ +pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + exclusiveContent { + forRepository { + maven { + name = 'Fabric' + url = uri('https://maven.fabricmc.net') + } + } + filter { + includeGroup('net.fabricmc') + includeGroup('fabric-loom') + } + } + exclusiveContent { + forRepository { + maven { + name = 'Sponge' + url = uri('https://repo.spongepowered.org/repository/maven-public') + } + } + filter { + includeGroupAndSubgroups("org.spongepowered") + } + } + exclusiveContent { + forRepository { + maven { + name = 'Forge' + url = uri('https://maven.minecraftforge.net') + } + } + filter { + includeGroupAndSubgroups('net.minecraftforge') + } + } + } +} + +plugins { + id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0' +} + +// This should match the folder name of the project, or else IDEA may complain (see https://youtrack.jetbrains.com/issue/IDEA-317606) +rootProject.name = 'concentration-reloaded' +include('common') +include('fabric') +include('neoforge') +include('forge') \ No newline at end of file