This commit is contained in:
deechael 2024-10-05 14:59:54 +08:00
commit 4c52efa3df
65 changed files with 3575 additions and 0 deletions

15
.gitattributes vendored Normal file
View File

@ -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

27
.gitignore vendored Normal file
View File

@ -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/

121
LICENSE Normal file
View File

@ -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.

4
README.md Normal file
View File

@ -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.

6
build.gradle Normal file
View File

@ -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
}

3
buildSrc/build.gradle Normal file
View File

@ -0,0 +1,3 @@
plugins {
id 'groovy-gradle-plugin'
}

View File

@ -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')
}
}
}

View File

@ -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)
}

41
common/build.gradle Normal file
View File

@ -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
}

View File

@ -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();
}
}
}

View File

@ -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() {
}
}

View File

@ -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<FullscreenMode> CODEC = StringRepresentable.fromEnum(FullscreenMode::values);
public static final IntFunction<FullscreenMode> 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;
}
}

View File

@ -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<ConfigListEntry> {
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<GuiEventListener> hovered = getChildAt(mouseX, mouseY);
return hovered.map(guiEventListener -> ((ConfigListEntry) guiEventListener).getHoveredStyle(mouseX, mouseY)).orElse(null);
}
}
public static class ConfigListEntry extends ContainerObjectSelectionList.Entry<ConfigListEntry> {
private final List<AbstractWidget> buttons;
public ConfigListEntry(List<AbstractWidget> 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<? extends NarratableEntry> narratables() {
return buttons;
}
@Override
public List<? extends GuiEventListener> 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<? extends NarratableEntry> 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<String> getter, Consumer<String> setter, Predicate<String> 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<String> getter, Consumer<String> setter) {
addTextField(description, getter, setter, Objects::nonNull);
}
public void addTextField(Component description, Supplier<String> getter, Consumer<String> setter, Predicate<String> validator) {
entries.addEntry(new ConfigListTextField(font, width / 2 - 154, 0, 308, 18, description, getter, setter, validator));
}
public void addIntField(Component description, Supplier<Integer> getter, Consumer<Integer> 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<Float> getter, Consumer<Float> setter) {
addTextField(description, () -> getter.get().toString(), value -> setter.accept(Float.parseFloat(value)), value -> {
try {
Float.parseFloat(value);
return true;
} catch (NumberFormatException e) {
return false;
}
});
}
}

View File

@ -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();
}

View File

@ -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();
}
}

View File

@ -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
}
}

View File

@ -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<OptionInstance<?>[]> 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<FullscreenMode> 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<Boolean> 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();
}

View File

@ -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);
}

View File

@ -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"
}

View File

@ -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": "Нативный"
}

View File

@ -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": "原生"
}

View File

@ -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": "原生"
}

View File

@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 B

View File

@ -0,0 +1,6 @@
{
"pack": {
"description": "${mod_name} Resources",
"pack_format": 8
}
}

49
fabric/build.gradle Normal file
View File

@ -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')
}
}
}

View File

@ -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();
}
}

View File

@ -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;
}

View File

@ -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<String> myTargets, Set<String> otherTargets) {
}
@Override
public List<String> 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");
}
}

View File

@ -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);
}
};
};
}
}

View File

@ -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) {
}
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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;<init>(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<Boolean> fullscreenOption = (Option<Boolean>) newOptions[3];
fullscreenOption.setOnApply(value -> {
Concentration.toggleFullScreenMode(minecraftOptions, value);
});
args.set(1, newOptions);
}
}
}

View File

@ -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;<init>(Lnet/minecraft/network/chat/Component;Lcom/google/common/collect/ImmutableList;)V"), index = 1)
private static ImmutableList<OptionGroup> inject$general(ImmutableList<OptionGroup> groups) {
List<OptionGroup> 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);
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1 @@
net.deechael.concentration.fabric.config.FabricConfigProvider

View File

@ -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
}
}

View File

@ -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"
}
}

89
forge/build.gradle Normal file
View File

@ -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
}

View File

@ -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);
}
}));
}
}
}

View File

@ -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<FullscreenMode> 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();
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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"

View File

@ -0,0 +1 @@
net.deechael.concentration.forge.config.ForgeConfigProvider

View File

@ -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
}
}

27
gradle.properties Normal file
View File

@ -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

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@ -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

249
gradlew vendored Normal file
View File

@ -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" "$@"

92
gradlew.bat vendored Normal file
View File

@ -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

50
neoforge/build.gradle Normal file
View File

@ -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"
}

View File

@ -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);
}
};
}
});
}
}
}

View File

@ -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<String> myTargets, Set<String> otherTargets) {
}
@Override
public List<String> 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");
}
}

View File

@ -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;
}
}
}
});
}
}

View File

@ -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<FullscreenMode> 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();
}
}

View File

@ -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();
}
}

View File

@ -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;<init>(Lnet/minecraft/network/chat/Component;Lcom/google/common/collect/ImmutableList;)V"), index = 1)
private static ImmutableList<OptionGroup> inject$general(ImmutableList<OptionGroup> groups) {
List<OptionGroup> 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);
}
}

View File

@ -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;
}
}

View File

@ -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"

View File

@ -0,0 +1 @@
net.deechael.concentration.neoforge.config.NeoForgeConfigProvider

View File

@ -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
}
}

51
settings.gradle Normal file
View File

@ -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')