Open DevTools
diff --git a/app.js b/app.js
index 011f731e..507d9fc3 100644
--- a/app.js
+++ b/app.js
@@ -3,17 +3,14 @@ const Utils = require("./utils");
const Config = require("./config");
const launcher = Launcher.instance;
const { ipcRenderer, shell } = require("electron");
-const { MicrosoftAuthService, YggdrasilAuthService, Account, AccountManager } = require("./auth");
+const { MicrosoftAuthService, Account, AccountManager } = require("./auth");
const microsoftAuthService = MicrosoftAuthService.instance;
-const yggdrasilAuthService = YggdrasilAuthService.instance;
const fs = require("fs");
const msmc = require("msmc");
const os = require("os");
const nbt = require("nbt");
-const vm = require("vm");
-const url = require("url");
const path = require("path");
-const axios = require("axios");
+const xss = require("xss");
Utils.init();
Config.init(Utils.dataDirectory);
@@ -31,7 +28,7 @@ ipcRenderer.on("quitGame", (event) => {
ipcRenderer.send("quit", true);
});
-window.addEventListener("DOMContentLoaded", () => {
+window.addEventListener("DOMContentLoaded", async() => {
if(Utils.getOsName() == "osx") {
document.querySelector(".drag-region").style.display = "block";
}
@@ -39,11 +36,9 @@ window.addEventListener("DOMContentLoaded", () => {
const playButton = document.getElementById("launch-button");
const launchNote = document.getElementById("launch-note");
const microsoftLoginButton = document.querySelector(".microsoft-login-button");
- const mojangLoginButton = document.querySelector(".mojang-login-button");
const accountButton = document.querySelector(".account-button");
const login = document.querySelector(".login");
- const mojangLogin = document.querySelector(".mojang-login");
const main = document.querySelector(".main");
const accounts = document.querySelector(".accounts");
const backToMain = document.querySelector(".back-to-main-button");
@@ -57,24 +52,24 @@ window.addEventListener("DOMContentLoaded", () => {
}
})
.then(async(response, error) => {
- var today = new Date();
+ let today = new Date();
- news.innerHTML = "
" + await response.text();
- for(var timeElement of news.getElementsByTagName("time")) {
- var datetime = timeElement.getAttribute("datetime");
+ news.innerHTML = "
" + xss(await response.text(), { whiteList: { ...xss.whiteList, time: ["datetime"] } });
+ for(let timeElement of news.getElementsByTagName("time")) {
+ let datetime = timeElement.getAttribute("datetime");
if(datetime == "future") {
timeElement.innerText = "The Future";
continue;
}
- var todayYear = today.getFullYear();
- var todayMonth = today.getMonth() + 1;
- var todayDay = today.getDate();
+ let todayYear = today.getFullYear();
+ let todayMonth = today.getMonth() + 1;
+ let todayDay = today.getDate();
- var year = parseInt(datetime.substring(0, 4));
- var month = parseInt(datetime.substring(5, 7));
- var day = parseInt(datetime.substring(8, 10));
+ let year = parseInt(datetime.substring(0, 4));
+ let month = parseInt(datetime.substring(5, 7));
+ let day = parseInt(datetime.substring(8, 10));
if(todayYear == year && todayMonth == month) {
if(todayDay == day) {
@@ -88,7 +83,7 @@ window.addEventListener("DOMContentLoaded", () => {
}
- var friendlyName = day + " " + monthNames[month - 1];
+ let friendlyName = day + " " + monthNames[month - 1];
if(today.getFullYear() != year) {
friendlyName += " " + year;
@@ -112,15 +107,29 @@ window.addEventListener("DOMContentLoaded", () => {
}
});
+ for(let account of launcher.accountManager.accounts) {
+ await launcher.accountManager.storeInKeychain(account);
+ }
+
function updateAccount() {
- document.querySelector(".account-button").innerHTML = `
${launcher.accountManager.activeAccount.username} `;
+ if(launcher.accountManager.activeAccount) {
+ document.querySelector(".account-button").innerHTML = `
${launcher.accountManager.activeAccount.username} `;
+ }
}
function updateMinecraftFolder() {
- document.querySelector(".minecraft-folder-path").innerText = Config.data.minecraftFolder;
+ document.querySelector(".minecraft-folder-path").innerText =
+ Config.data.minecraftFolder ?? "(use default)";
+ // wow. I didn't know you could do that in js.
+ }
+
+ function updateJre() {
+ document.querySelector(".jre-location").innerText =
+ Config.data.jrePath ?? "(download automatically)"
}
updateMinecraftFolder();
+ updateJre();
backToMain.onclick = () => {
if(loggingIn) {
@@ -140,8 +149,8 @@ window.addEventListener("DOMContentLoaded", () => {
backToMain.style.display = null;
}
- var launching = false;
- var loggingIn = false;
+ let launching = false;
+ let loggingIn = false;
document.onmousedown = (event) => {
if(!main.style.display) {
@@ -157,13 +166,13 @@ window.addEventListener("DOMContentLoaded", () => {
accounts.innerHTML = "";
for(let account of launcher.accountManager.accounts) {
- var accountElement = document.createElement("div");
+ let accountElement = document.createElement("div");
accountElement.classList.add("account");
accountElement.innerHTML = `
${account.username} `;
- accountElement.onclick = (event) => {
+ accountElement.onclick = async(event) => {
if(event.target.classList.contains("remove-account")
|| event.target.parentElement.classList.contains("remove-account")) {
- if(!launcher.accountManager.removeAccount(account)) {
+ if(!(await launcher.accountManager.removeAccount(account))) {
main.style.display = null;
login.style.display = "block";
backToMain.style.display = null;
@@ -181,7 +190,7 @@ window.addEventListener("DOMContentLoaded", () => {
accounts.appendChild(accountElement);
}
- var addElement = document.createElement("div");
+ let addElement = document.createElement("div");
addElement.classList.add("account");
addElement.innerHTML = `
Add Account `;
addElement.onclick = () => {
@@ -212,7 +221,7 @@ window.addEventListener("DOMContentLoaded", () => {
}
};
- ipcRenderer.on("msa", (event, result) => {
+ ipcRenderer.on("msa", async(event, result) => {
loggingIn = false;
microsoftLoginButton.innerText = "Microsoft Account";
result = JSON.parse(result);
@@ -223,7 +232,7 @@ window.addEventListener("DOMContentLoaded", () => {
alert("Could not log in: " + result.type);
return;
}
- var account = microsoftAuthService.authenticate(result.profile);
+ let account = await microsoftAuthService.authenticate(result.profile);
launcher.accountManager.addAccount(account);
login.style.display = "none";
main.style.display = "block";
@@ -231,13 +240,6 @@ window.addEventListener("DOMContentLoaded", () => {
updateAccounts();
})
- mojangLoginButton.onclick = () => {
- if(!loggingIn) {
- login.style.display = "none";
- mojangLogin.style.display = "block";
- }
- };
-
async function play(server) {
if(!launching) {
launching = true;
@@ -272,29 +274,43 @@ window.addEventListener("DOMContentLoaded", () => {
playButton.onclick = () => play();
- document.querySelector(".back-to-login-button").onclick = () => {
- mojangLogin.style.display = "none";
- login.style.display = "block";
- };
-
document.querySelector(".about-tab").onclick = () => switchToTab("about");
document.querySelector(".settings-tab").onclick = () => switchToTab("settings");
document.querySelector(".news-tab").onclick = () => switchToTab("news");
- document.querySelector(".minecraft-folder").onclick = () => ipcRenderer.send("directory");
-
- ipcRenderer.on("directory", (event, file) => {
- Config.data.minecraftFolder = file;
+ document.querySelector(".minecraft-folder").onclick = () => ipcRenderer.send("directory", "Select Minecraft Folder", "minecraft");
+ document.querySelector(".jre-location-change").onclick = () => ipcRenderer.send("directory", "Select JRE Folder", "jre");
+ document.querySelector(".jre-location-reset").onclick = () => {
+ Config.data.jrePath = null;
Config.save();
- updateMinecraftFolder();
- updateServers();
+ updateJre();
+ };
+
+ ipcRenderer.on("directory", (event, file, id) => {
+ switch(id) {
+ case "minecraft":
+ Config.data.minecraftFolder = file;
+ Config.save();
+ updateMinecraftFolder();
+ updateServers();
+ break;
+ case "jre":
+ if(!fs.existsSync(path.join(file, "bin/java"))) {
+ ipcRenderer.send("jreError");
+ return;
+ }
+ Config.data.jrePath = file;
+ Config.save();
+ updateJre();
+ break;
+ }
});
document.querySelector(".devtools").onclick = () => ipcRenderer.send("devtools");
function updateServers() {
- var serversList = document.querySelector(".quick-servers");
- var serversFile = Config.getGameDirectory(Utils.gameDirectory) + "/servers.dat";
- var serverText = document.querySelector(".quick-join-text");
+ let serversList = document.querySelector(".quick-servers");
+ let serversFile = Config.getGameDirectory(Utils.gameDirectory) + "/servers.dat";
+ let serverText = document.querySelector(".quick-join-text");
if(fs.existsSync(serversFile)) {
nbt.parse(fs.readFileSync(serversFile), (error, data) => {
@@ -302,13 +318,13 @@ window.addEventListener("DOMContentLoaded", () => {
throw error;
}
- var servers = data.value.servers.value.value;
+ let servers = data.value.servers.value.value;
if(servers.length > 0) {
serversList.innerHTML = "";
}
- for(var i = 0; i < servers.length && i < 5; i++) {
+ for(let i = 0; i < servers.length && i < 5; i++) {
// first time I've ever needed to use the let keyword
let server = servers[i];
let serverIndex = i;
@@ -339,26 +355,33 @@ window.addEventListener("DOMContentLoaded", () => {
updateServers();
- var memory = document.querySelector(".memory");
- var memoryLabel = document.querySelector(".memory-label");
+ let memory = document.querySelector(".memory");
+ let memoryLabel = document.querySelector(".memory-label");
memory.max = os.totalmem() / 1024 / 1024;
memory.value = Config.data.maxMemory;
- var optifine = document.querySelector(".optifine");
+ let optifine = document.querySelector(".optifine");
optifine.checked = Config.data.optifine;
optifine.onchange = () => {
Config.data.optifine = optifine.checked;
Config.save();
};
- var autoUpdate = document.querySelector(".auto-update");
+ let autoUpdate = document.querySelector(".auto-update");
autoUpdate.checked = Config.data.autoUpdate;
autoUpdate.onchange = () => {
Config.data.autoUpdate = autoUpdate.checked;
Config.save();
};
+ let jvmArguments = document.querySelector(".jvm-arguments");
+ jvmArguments.value = Config.data.jvmArgs;
+ jvmArguments.onchange = () => {
+ Config.data.jvmArgs = jvmArguments.value;
+ Config.save();
+ };
+
function updateMemoryLabel() {
memoryLabel.innerText = (memory.value / 1024).toFixed(1) + " GB";
Config.data.maxMemory = memory.value;
@@ -369,7 +392,7 @@ window.addEventListener("DOMContentLoaded", () => {
updateMemoryLabel();
- var currentTab = "about";
+ let currentTab = "about";
function switchToTab(tab) {
document.querySelector("." + currentTab).style.display = "none";
@@ -385,35 +408,11 @@ window.addEventListener("DOMContentLoaded", () => {
currentTab = tab;
}
- const loginButtonMojang = document.querySelector(".login-button-mojang");
const emailField = document.getElementById("username");
const passwordField = document.getElementById("password");
const errorMessage = document.querySelector(".error-message");
- loginButtonMojang.onclick = async() => {
- if(!loggingIn) {
- loggingIn = true;
- loginButtonMojang.innerText = "...";
- try {
- var account = await yggdrasilAuthService.authenticateUsernamePassword(emailField.value, passwordField.value);
- launcher.accountManager.addAccount(account);
- mojangLogin.style.display = "none";
- main.style.display = "block";
- emailField.value = "";
- passwordField.value = "";
- errorMessage.innerText = "";
- updateAccount();
- updateAccounts();
- }
- catch(error) {
- errorMessage.innerText = "Could not log in";
- }
- loginButtonMojang.innerText = "Log In";
- loggingIn = false;
- }
- };
-
- for(var element of document.querySelectorAll(".open-in-browser")) {
+ for(let element of document.querySelectorAll(".open-in-browser")) {
const href = element.href;
element.href = "javascript:void(0);";
element.onclick = function(event) {
diff --git a/assets/screenshots/Launcher.png b/assets/screenshots/Launcher.png
new file mode 100644
index 00000000..be9fa21f
Binary files /dev/null and b/assets/screenshots/Launcher.png differ
diff --git a/assets/screenshots/Mods.png b/assets/screenshots/Mods.png
new file mode 100644
index 00000000..deb4b830
Binary files /dev/null and b/assets/screenshots/Mods.png differ
diff --git a/assets/screenshots/README.md b/assets/screenshots/README.md
deleted file mode 100644
index f405b5a6..00000000
--- a/assets/screenshots/README.md
+++ /dev/null
@@ -1,13 +0,0 @@
-# Screenshots
-
-
-
-
-
-
-
-
-
-
-
-Note: I used inspect element to fix a glitch on my system, and a combination of the development version, and the older version that didn't have a bug.
\ No newline at end of file
diff --git a/assets/sol-client.desktop b/assets/sol-client.desktop
new file mode 100644
index 00000000..60f9c944
--- /dev/null
+++ b/assets/sol-client.desktop
@@ -0,0 +1,10 @@
+[Desktop Entry]
+
+Name=Sol Client
+Comment=Simple yet feature-rich open-source Minecraft client.
+GenericName=Sol Client
+Exec=sol-client %U
+Icon=sol-client.png
+Type=Application
+StartupNotify=true
+Categories=Game;
diff --git a/auth.js b/auth.js
index ac15de82..79e9c81e 100644
--- a/auth.js
+++ b/auth.js
@@ -1,31 +1,89 @@
const axios = require("axios");
const msmc = require("msmc");
const fs = require("fs");
+const keytar = require("keytar");
const Utils = require("./utils");
+const KEYCHAIN_PREFIX = "keychain:";
+const SERVICE = "sol_client";
+let manager;
class AccountManager {
constructor(file, dataCallback) {
this.file = file;
if(fs.existsSync(file)) {
- var data = JSON.parse(fs.readFileSync(file, "UTF-8"));
+ let data = JSON.parse(fs.readFileSync(file, "UTF-8"));
this.accounts = [];
- for(var account of data.accounts) {
+ for(let account of data.accounts) {
+ if(account.type != "msa") {
+ continue;
+ }
+
this.accounts.push(Account.from(account));
}
this.activeAccount = this.accounts[data.activeAccount];
- this.fetchSkin(this.activeAccount);
+ if(this.activeAccount) {
+ this.fetchSkin(this.activeAccount);
+ }
}
else {
this.accounts = [];
- this.save();
}
+ this.save();
this.dataCallback = dataCallback;
this.refreshTask = {};
+ manager = this;
}
save() {
- fs.writeFileSync(this.file, JSON.stringify({ accounts: this.accounts, activeAccount: this.accounts.indexOf(this.activeAccount) }));
+ fs.writeFileSync(this.file, JSON.stringify({
+ accounts: this.accounts,
+ activeAccount: this.accounts.indexOf(this.activeAccount)
+ }));
+ }
+
+ isInKeychain(prop) {
+ return prop.startsWith(KEYCHAIN_PREFIX);
+ }
+
+ async storeInKeychain(account) {
+ account.accessToken = await this.storeProp(account.accessToken, account.uuid + "_access_token");
+ if(account._msmc) {
+ account._msmc.refresh = await this.storeProp(account._msmc.refresh, account.uuid + "_refresh");
+ account._msmc.mcToken = undefined; // ah yes, the number underfined
+ }
+ }
+
+ async storeProp(prop, key) {
+ if(this.isInKeychain(prop)) {
+ return prop;
+ }
+ await keytar.setPassword(SERVICE, key, prop);
+ let test = await keytar.getPassword(SERVICE, key);
+ if(test != prop) {
+ return prop;
+ }
+ return KEYCHAIN_PREFIX + key;
+ }
+
+ async retrieveProp(prop) {
+ if(!this.isInKeychain(prop)) {
+ return prop;
+ }
+ let key = prop.substring(KEYCHAIN_PREFIX.length);
+ return keytar.getPassword(SERVICE, key);
+ }
+
+ async realToken(account) {
+ return this.retrieveProp(account.accessToken);
+ }
+
+ async realRefresh(account) {
+ if(!account._msmc) {
+ return null;
+ }
+
+ return this.retrieveProp(account._msmc.refresh);
}
refreshAccount(account) {
@@ -33,10 +91,10 @@ class AccountManager {
return this.refreshTask;
}
- var task = new Promise(async(resolve, reject) => {
- var valid = await this.activeAccount.getService().validate(this.activeAccount);
+ let task = new Promise(async(resolve, reject) => {
+ let valid = await this.activeAccount.getService().validate(this.activeAccount);
if(!valid) {
- var result = await this.activeAccount.getService().refresh(this.activeAccount);
+ let result = await this.activeAccount.getService().refresh(this.activeAccount);
if(!result) {
this.removeAccount(this.activeAccount);
reject();
@@ -70,16 +128,16 @@ class AccountManager {
}
async fetchSkin(account) {
- var textures = await Utils.getTextures(account.uuid);
+ let textures = await Utils.getTextures(account.uuid);
if(textures) {
if(textures.SKIN) {
account.skin = await Utils.expandImageURL(textures.SKIN.url);
- var image = await Utils.loadImage(account.skin);
+ let image = await Utils.loadImage(account.skin);
- var canvas = document.createElement("canvas");
- var context = canvas.getContext("2d");
+ let canvas = document.createElement("canvas");
+ let context = canvas.getContext("2d");
canvas.width = 8;
canvas.height = 8;
@@ -112,10 +170,10 @@ class AccountManager {
}
addAccount(account) {
- var sameUUIDIndex = -1;
+ let sameUUIDIndex = -1;
- for(var i = 0; i < this.accounts.length; i++) {
- var item = this.accounts[i];
+ for(let i = 0; i < this.accounts.length; i++) {
+ let item = this.accounts[i];
if(item.uuid == account.uuid) {
sameUUIDIndex = i;
}
@@ -131,10 +189,13 @@ class AccountManager {
this.switchAccount(account);
}
- removeAccount(account) {
- var index = this.accounts.indexOf(this.activeAccount);
+ async removeAccount(account) {
+ let index = this.accounts.indexOf(this.activeAccount);
this.accounts = this.accounts.filter((item) => item != account);
+ keytar.deletePassword(SERVICE, KEYCHAIN_PREFIX + account.uuid + "_access_token");
+ keytar.deletePassword(SERVICE, KEYCHAIN_PREFIX + account.uuid + "_refresh");
+
if(account == this.activeAccount) {
this.activeAccount = this.accounts[index];
@@ -154,137 +215,47 @@ class AccountManager {
class AuthService {
- authenticate(key) {
+ authenticate(_key) {
throw new Error("Unimplemented");
}
}
-class YggdrasilAuthKey {
-
- constructor(username, password) {
- this.username = username;
- this.password = password;
- }
-
-}
-
class MicrosoftAuthService extends AuthService {
static instance = new MicrosoftAuthService();
- authenticate(msmc) {
- return new Account("msa", msmc.name, msmc.id, msmc._msmc.mcToken, null, msmc._msmc.demo, msmc._msmc);
+ async authenticate(msmc) {
+ let account = new Account("msa", msmc.name, msmc.id, msmc._msmc.mcToken, null, msmc._msmc.demo, msmc._msmc);
+ msmc._msmc.mcToken = undefined;
+ await manager.storeInKeychain(account);
+ return account;
}
- toMsmc(account) {
+ async toMsmc(account) {
return {
name: account.username,
id: account.uuid,
- _msmc: account._msmc
+ _msmc: { ...account._msmc, ...{ refresh: await manager.realRefresh(account), mcToken: await manager.realToken(account) } }
}
}
validate(account) {
- return new Promise((resolve) => {
- resolve(msmc.validate(this.toMsmc(account)));
+ return new Promise(async(resolve) => {
+ resolve(msmc.validate(await this.toMsmc(account)));
});
}
refresh(account) {
return new Promise(async(resolve) => {
- var result = await msmc.refresh(this.toMsmc(account), () => {}, {client_id: "00000000402b5328"});
+ let result = await msmc.refresh(await this.toMsmc(account), () => {}, {client_id: "00000000402b5328"});
if(result.type != "Success") {
resolve(null);
return;
}
- resolve(this.authenticate(result.profile));
- });
- }
-
-}
-
-class YggdrasilAuthService extends AuthService {
-
- static instance = new YggdrasilAuthService();
- static api = "https://authserver.mojang.com";
-
- constructor() {
- super();
- }
-
- authenticateUsernamePassword(username, password) {
- return this.authenticate(new YggdrasilAuthKey(username, password));
- }
-
- authenticate(key) {
- const url = YggdrasilAuthService.api;
- return new Promise((resolve, reject) => {
- axios.post(url + "/authenticate", {
- "agent": {
- "name": "Minecraft",
- "version": 1
- },
- "username": key.username,
- "password": key.password
- })
- .catch((error) => {
- reject(error);
- })
- .then((response) => {
- if(response == null) {
- return;
- }
- var data = response.data;
- resolve(
- new Account(
- "mojang",
- data.selectedProfile.name,
- data.selectedProfile.id,
- data.accessToken,
- data.clientToken
- )
- );
- });
- });
- }
-
- validate(account) {
- return new Promise((resolve) => {
- const url = YggdrasilAuthService.api;
- axios.post(url + "/validate", {
- accessToken: account.accessToken,
- clientToken: account.clientToken
- })
- .catch((error) => {
- resolve(false);
- })
- .then((response) => {
- resolve(true);
- });
- });
- }
-
- refresh(account) {
- const url = YggdrasilAuthService.api;
- return new Promise((resolve) => {
- axios.post(url + "/refresh", {
- accessToken: account.accessToken,
- clientToken: account.clientToken,
- selectedProfile: {
- name: account.username,
- id: account.uuid
- }
- })
- .catch((error) => {
- resolve(false);
- })
- .then((response) => {
- account.accessToken = response.data.accessToken;
- resolve(true);
- });
+ resolve(await this.authenticate(result.profile));
});
}
@@ -312,13 +283,12 @@ class Account {
return MicrosoftAuthService.instance;
}
else {
- return YggdrasilAuthService.instance;
+ throw new Error("Unsupported account type " + this.type);
}
}
}
-exports.YggdrasilAuthService = YggdrasilAuthService;
exports.MicrosoftAuthService = MicrosoftAuthService;
exports.Account = Account;
exports.AccountManager = AccountManager;
diff --git a/config.js b/config.js
index d79489d2..fe7c5e9f 100644
--- a/config.js
+++ b/config.js
@@ -3,12 +3,16 @@ const Utils = require("./utils");
class Config {
- static data = {
+ static DEFAULT = {
maxMemory: 2048,
optifine: Utils.getOsName() != "osx",
- minecraftFolder: "
",
- autoUpdate: true
+ minecraftFolder: null,
+ autoUpdate: true,
+ jvmArgs: "",
+ jrePath: null
};
+
+ static data = Config.DEFAULT;
static file;
static init(base) {
@@ -17,12 +21,10 @@ class Config {
static load() {
if(fs.existsSync(Config.file)) {
- Config.data = JSON.parse(fs.readFileSync(Config.file, "UTF-8"));
- if(!Config.data.minecraftFolder) {
- Config.data.minecraftFolder = "";
- }
- if(Config.data.autoUpdate == undefined) {
- Config.data.autoUpdate = true;
+ Config.data = { ...Config.DEFAULT, ...JSON.parse(fs.readFileSync(Config.file, "UTF-8")) };
+
+ if(Config.data.minecraftFolder == "") {
+ Config.data.minecraftFolder = null;
}
}
}
@@ -32,13 +34,44 @@ class Config {
}
static getGameDirectory(defaultDirectory) {
- if(Config.data.minecraftFolder != "") {
+ if(Config.data.minecraftFolder) {
return Config.data.minecraftFolder;
}
return defaultDirectory;
}
+ static getJvmArgs() {
+ let args = Config.data.jvmArgs;
+ let result = [];
+
+ let prevC;
+ let arg = "";
+
+ for(let c of args) {
+ if(prevC == "\\") {
+ arg = arg.substring(0, arg.length - 1);
+ arg += c;
+ prevC = 0;
+ continue;
+ }
+ else if(c == " ") {
+ result.push(arg);
+ arg = "";
+ }
+ else {
+ arg += c;
+ }
+ prevC = c;
+ }
+
+ if(arg !== "") {
+ result.push(arg);
+ }
+
+ return result;
+ }
+
}
module.exports = Config;
diff --git a/game/src/main/java/io/github/solclient/client/Client.java b/game/src/main/java/io/github/solclient/client/Client.java
index d1b1509a..ef9e12ba 100755
--- a/game/src/main/java/io/github/solclient/client/Client.java
+++ b/game/src/main/java/io/github/solclient/client/Client.java
@@ -9,7 +9,6 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
-import java.util.function.Supplier;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.ArrayUtils;
@@ -30,6 +29,7 @@
import io.github.solclient.client.culling.CullTask;
import io.github.solclient.client.event.EventBus;
import io.github.solclient.client.event.EventHandler;
+import io.github.solclient.client.event.impl.GameQuitEvent;
import io.github.solclient.client.event.impl.PostGameStartEvent;
import io.github.solclient.client.event.impl.PreTickEvent;
import io.github.solclient.client.event.impl.SendChatMessageEvent;
@@ -57,15 +57,15 @@
import io.github.solclient.client.mod.impl.hud.CoordinatesMod;
import io.github.solclient.client.mod.impl.hud.CpsMod;
import io.github.solclient.client.mod.impl.hud.FpsMod;
-import io.github.solclient.client.mod.impl.hud.PingMod;
import io.github.solclient.client.mod.impl.hud.PotionEffectsMod;
import io.github.solclient.client.mod.impl.hud.ReachDisplayMod;
import io.github.solclient.client.mod.impl.hud.ScoreboardMod;
-import io.github.solclient.client.mod.impl.hud.SpeedometerMod;
import io.github.solclient.client.mod.impl.hud.armour.ArmourMod;
import io.github.solclient.client.mod.impl.hud.chat.ChatMod;
import io.github.solclient.client.mod.impl.hud.crosshair.CrosshairMod;
import io.github.solclient.client.mod.impl.hud.keystrokes.KeystrokesMod;
+import io.github.solclient.client.mod.impl.hud.ping.PingMod;
+import io.github.solclient.client.mod.impl.hud.speedometer.SpeedometerMod;
import io.github.solclient.client.mod.impl.hud.tablist.TabListMod;
import io.github.solclient.client.mod.impl.hud.timers.TimersMod;
import io.github.solclient.client.mod.impl.hypixeladditions.HypixelAdditionsMod;
@@ -78,11 +78,11 @@
import io.github.solclient.client.ui.screen.mods.MoveHudsScreen;
import io.github.solclient.client.util.Utils;
import io.github.solclient.client.util.access.AccessMinecraft;
+import io.github.solclient.client.util.font.SlickFontRenderer;
import lombok.Getter;
import lombok.Setter;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiMainMenu;
-import net.minecraft.client.gui.GuiScreen;
import net.minecraft.client.multiplayer.ServerData;
import net.minecraft.client.multiplayer.WorldClient;
import net.minecraft.client.resources.IResource;
@@ -100,18 +100,20 @@
*/
public class Client {
- private Minecraft mc = Minecraft.getMinecraft();
public static final Client INSTANCE = new Client();
+
+ private final Minecraft mc = Minecraft.getMinecraft();
private JsonObject data;
@Getter
private List mods = new ArrayList();
private Map modsById = new HashMap<>();
@Getter
private List huds = new ArrayList();
- private static final Logger LOGGER = LogManager.getLogger();
+ public static final Logger LOGGER = LogManager.getLogger();
- private final File DATA_FILE = new File(Minecraft.getMinecraft().mcDataDir, "sol_client_mods.json");
- private final File LEGACY_DATA_FILE = new File(Minecraft.getMinecraft().mcDataDir, "parrot_client_mods.json" /* This was the old name. */ );
+ private final File DATA_FILE = new File(mc.mcDataDir, "sol_client_mods.json");
+ // data file for beta versions - this is no longer very neccessary.
+ private final File LEGACY_DATA_FILE = new File(mc.mcDataDir, "parrot_client_mods.json");
public DetectedServer detectedServer;
@@ -140,8 +142,7 @@ public class Client {
public void init() {
Utils.resetLineWidth();
new File(mc.mcDataDir, "server-resource-packs").mkdirs(); // Fix crash
-
- System.setProperty("http.agent", "Sol Client/" + VERSION);
+ System.setProperty("http.agent", "Sol Client/" + Client.VERSION);
LOGGER.info("Initialising...");
bus.register(this);
@@ -457,6 +458,11 @@ else if(SolClientMod.instance.editHudKey.isPressed()) {
}
}
+ @EventHandler
+ public void onQuit(GameQuitEvent event) {
+ SlickFontRenderer.DEFAULT.free();
+ }
+
public void registerChatButton(ChatButton button) {
chatButtons.add(button);
chatButtons.sort(Comparator.comparingInt(ChatButton::getPriority));
@@ -495,7 +501,7 @@ public List getChatButtons() {
* Saves if the mod screen is not opened.
*/
public void optionChanged() {
- if(!(Minecraft.getMinecraft().currentScreen instanceof ModsScreen)) {
+ if(!(mc.currentScreen instanceof ModsScreen)) {
save();
}
}
diff --git a/game/src/main/java/io/github/solclient/client/PreMain.java b/game/src/main/java/io/github/solclient/client/PreMain.java
new file mode 100644
index 00000000..0f673659
--- /dev/null
+++ b/game/src/main/java/io/github/solclient/client/PreMain.java
@@ -0,0 +1,30 @@
+package io.github.solclient.client;
+
+import org.lwjgl.LWJGLUtil;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import io.github.solclient.client.util.Utils;
+import net.minecraft.client.main.Main;
+import net.minecraft.launchwrapper.Launch;
+
+public class PreMain {
+
+ public static void main(String[] args) {
+ if(LWJGLUtil.getPlatform() == LWJGLUtil.PLATFORM_LINUX) {
+ preload("org.lwjgl.opengl.LinuxKeycodes");
+ }
+ Main.main(args);
+ }
+
+ private static void preload(String name) {
+ try {
+ Class.forName(name, true, Launch.classLoader);
+ }
+ catch(Exception error) {
+ LogManager.getLogger().error("Could not preload " + name + ". This may cause further issues.", error);
+ }
+ }
+
+}
diff --git a/game/src/main/java/io/github/solclient/client/event/EventBus.java b/game/src/main/java/io/github/solclient/client/event/EventBus.java
index 59231186..f34776b6 100755
--- a/game/src/main/java/io/github/solclient/client/event/EventBus.java
+++ b/game/src/main/java/io/github/solclient/client/event/EventBus.java
@@ -1,5 +1,8 @@
package io.github.solclient.client.event;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.WrongMethodTypeException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
@@ -28,26 +31,29 @@ public class EventBus {
private static class MethodData {
public Object instance;
- public Method target;
+ public String name;
+ public MethodHandle target;
- public MethodData(Object instance, Method target) {
+ public MethodData(Object instance, Method method) throws IllegalAccessException {
this.instance = instance;
- this.target = target;
+ name = method.getName();
+ target = MethodHandles.lookup().unreflect(method);
}
}
+ private static final Logger LOGGER = LogManager.getLogger();
+
private boolean inPost;
private List toRemove = new ArrayList<>();
private Map, List> handlers = new HashMap, List>();
- private Logger logger = LogManager.getLogger();
private Set getMethods(Class> clazz) {
Set methods = new HashSet<>();
Collections.addAll(methods, clazz.getMethods());
Collections.addAll(methods, clazz.getDeclaredMethods());
-
+ // without this, Lookup throws an exception
methods.forEach((method) -> method.setAccessible(true));
return methods;
@@ -56,8 +62,13 @@ private Set getMethods(Class> clazz) {
public void register(Object obj) {
for(Method method : getMethods(obj.getClass())) {
if(validate(method)) {
- handlers.computeIfAbsent(method.getParameters()[0].getType(), (ignore) -> new ArrayList<>())
- .add(new MethodData(obj, method));
+ try {
+ handlers.computeIfAbsent(method.getParameters()[0].getType(), (ignore) -> new ArrayList<>())
+ .add(new MethodData(obj, method));
+ }
+ catch(IllegalAccessException error) {
+ LOGGER.error("Could not register " + method.getName(), error);
+ }
}
}
}
@@ -85,11 +96,11 @@ public T post(T event) {
try {
method.target.invoke(method.instance, event);
}
- catch(IllegalAccessException | IllegalArgumentException error) {
- logger.error("Failed to invoke " + method.target.getName() + ":", error);
+ catch(WrongMethodTypeException error) {
+ LOGGER.error("Failed to invoke " + method.name + ":", error);
}
- catch(InvocationTargetException error) {
- logger.error("Error while executing " + method.target.getName() + ":", error.getCause());
+ catch(Throwable error) {
+ LOGGER.error("Error while executing " + method.name + ":", error);
}
}
inPost = false;
diff --git a/game/src/main/java/io/github/solclient/client/mixin/MixinWorldInfo.java b/game/src/main/java/io/github/solclient/client/mixin/MixinWorldInfo.java
index 0e72092e..a96e5c15 100755
--- a/game/src/main/java/io/github/solclient/client/mixin/MixinWorldInfo.java
+++ b/game/src/main/java/io/github/solclient/client/mixin/MixinWorldInfo.java
@@ -8,14 +8,19 @@
import io.github.solclient.client.Client;
import io.github.solclient.client.event.impl.TimeEvent;
+import net.minecraft.client.Minecraft;
import net.minecraft.world.storage.WorldInfo;
@Mixin(WorldInfo.class)
public class MixinWorldInfo {
+ private Minecraft mc = Minecraft.getMinecraft();
+
@Inject(method = "getWorldTime", at = @At("HEAD"), cancellable = true)
public void overrideWorldTime(CallbackInfoReturnable callback) {
- callback.setReturnValue(Client.INSTANCE.bus.post(new TimeEvent(worldTime)).time);
+ if(mc.theWorld != null && (Object) this == mc.theWorld.getWorldInfo()) {
+ callback.setReturnValue(Client.INSTANCE.bus.post(new TimeEvent(worldTime)).time);
+ }
}
@Shadow
diff --git a/game/src/main/java/io/github/solclient/client/mixin/client/MixinScreenshotHelper.java b/game/src/main/java/io/github/solclient/client/mixin/client/MixinScreenshotHelper.java
index 6b5c1033..7d6a3b93 100644
--- a/game/src/main/java/io/github/solclient/client/mixin/client/MixinScreenshotHelper.java
+++ b/game/src/main/java/io/github/solclient/client/mixin/client/MixinScreenshotHelper.java
@@ -18,6 +18,7 @@
import com.replaymod.replay.ReplayModReplay;
+import io.github.solclient.client.util.Utils;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.GlStateManager;
import net.minecraft.client.renderer.OpenGlHelper;
@@ -110,12 +111,12 @@ public static IChatComponent saveScreenshot(File gameDirectory, String screensho
new ChatComponentText(" ").appendSibling(new ChatComponentText("[" + I18n.format("sol_client.screenshot.open_folder") + "]")
.setChatStyle(new ChatStyle().setColor(EnumChatFormatting.YELLOW)
.setChatClickEvent(new ClickEvent(ClickEvent.Action.OPEN_FILE,
- screenshot.getAbsolutePath() + "§scshowinfolder§")))));
+ screenshot.getAbsolutePath() + Utils.REVEAL_SUFFIX)))));
Minecraft.getMinecraft().ingameGUI.getChatGUI().printChatMessage(secondaryText);
}
catch(Exception error) {
- logger.warn("Couldn\'t save screenshot", error);
+ logger.warn("Couldn't save screenshot", error);
Minecraft.getMinecraft().ingameGUI.getChatGUI()
.printChatMessage(new ChatComponentTranslation("screenshot.failure", error.getMessage()));
}
@@ -131,7 +132,7 @@ public static IChatComponent saveScreenshot(File gameDirectory, String screensho
return null;
}
catch(Exception exception) {
- logger.warn("Couldn\'t save screenshot", exception);
+ logger.warn("Couldn't save screenshot", exception);
return new ChatComponentTranslation("screenshot.failure", exception.getMessage());
}
}
diff --git a/game/src/main/java/io/github/solclient/client/mixin/lwjgl/MixinLinuxKeyboard.java b/game/src/main/java/io/github/solclient/client/mixin/lwjgl/MixinLinuxKeyboard.java
new file mode 100644
index 00000000..9c7fdc80
--- /dev/null
+++ b/game/src/main/java/io/github/solclient/client/mixin/lwjgl/MixinLinuxKeyboard.java
@@ -0,0 +1,73 @@
+package io.github.solclient.client.mixin.lwjgl;
+
+import org.spongepowered.asm.mixin.Final;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Overwrite;
+import org.spongepowered.asm.mixin.Shadow;
+
+@Mixin(targets = "org.lwjgl.opengl.LinuxKeyboard")
+public class MixinLinuxKeyboard {
+
+ @Shadow
+ private @Final int numlock_mask, modeswitch_mask, caps_lock_mask, shift_lock_mask;
+
+ @Overwrite(remap = false)
+ private int getKeycode(long eventp, int eventState) {
+ /*
+ * Copyright (c) 2002-2008 LWJGL Project All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'LWJGL' nor the names of its contributors may be used
+ * to endorse or promote products derived from this software without specific
+ * prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+ boolean shift = (eventState & (1 | shift_lock_mask)) != 0;
+ int group = (eventState & modeswitch_mask) != 0 ? 1 : 0;
+ long keysym;
+ if((eventState & numlock_mask) != 0 && isKeypadKeysym(keysym = getKeySym(eventp, group, 1))) {
+ if(shift)
+ keysym = getKeySym(eventp, group, 0);
+ }
+ else {
+ keysym = getKeySym(eventp, group, 0);
+ if(shift ^ ((eventState & caps_lock_mask) != 0))
+ keysym = toUpper(keysym);
+ }
+ return MixinLinuxKeycodes.mapKeySymToLWJGLKeyCode(keysym);
+ }
+
+ @Shadow
+ private static boolean isKeypadKeysym(long keysym) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Shadow
+ private static long getKeySym(long eventp, int group, int index) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Shadow
+ private static native long toUpper(long keysym);
+
+}
diff --git a/game/src/main/java/io/github/solclient/client/mixin/lwjgl/MixinLinuxKeycodes.java b/game/src/main/java/io/github/solclient/client/mixin/lwjgl/MixinLinuxKeycodes.java
new file mode 100644
index 00000000..eaa1afd2
--- /dev/null
+++ b/game/src/main/java/io/github/solclient/client/mixin/lwjgl/MixinLinuxKeycodes.java
@@ -0,0 +1,14 @@
+package io.github.solclient.client.mixin.lwjgl;
+
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.gen.Invoker;
+
+@Mixin(targets = "org.lwjgl.opengl.LinuxKeycodes")
+public interface MixinLinuxKeycodes {
+
+ @Invoker(value = "mapKeySymToLWJGLKeyCode", remap = false)
+ static int mapKeySymToLWJGLKeyCode(long keysym) {
+ throw new UnsupportedOperationException();
+ }
+
+}
diff --git a/game/src/main/java/io/github/solclient/client/mixin/mod/MixinTweaksMod.java b/game/src/main/java/io/github/solclient/client/mixin/mod/MixinTweaksMod.java
index 5344c130..03c019ee 100644
--- a/game/src/main/java/io/github/solclient/client/mixin/mod/MixinTweaksMod.java
+++ b/game/src/main/java/io/github/solclient/client/mixin/mod/MixinTweaksMod.java
@@ -19,6 +19,7 @@
import net.minecraft.client.gui.GuiIngame;
import net.minecraft.client.gui.GuiIngameMenu;
import net.minecraft.client.gui.ScaledResolution;
+import net.minecraft.client.gui.inventory.GuiContainer;
import net.minecraft.client.renderer.EntityRenderer;
import net.minecraft.client.renderer.GlStateManager;
import net.minecraft.client.renderer.InventoryEffectRenderer;
@@ -29,6 +30,7 @@
import net.minecraft.client.settings.KeyBinding;
import net.minecraft.enchantment.Enchantment;
import net.minecraft.entity.Entity;
+import net.minecraft.inventory.Container;
import net.minecraft.item.ItemPotion;
import net.minecraft.item.ItemStack;
import net.minecraft.util.EnumChatFormatting;
@@ -113,7 +115,11 @@ public void overrideName(int level, CallbackInfoReturnable callback) {
}
@Mixin(InventoryEffectRenderer.class)
- public static class MixinInventoryEffectRenderer {
+ public static abstract class MixinInventoryEffectRenderer extends GuiContainer {
+
+ public MixinInventoryEffectRenderer(Container inventorySlotsIn) {
+ super(inventorySlotsIn);
+ }
@Redirect(method = "drawActivePotionEffects", at = @At(value = "INVOKE",
target = "Lnet/minecraft/client/resources/I18n;format(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;"))
@@ -125,6 +131,16 @@ public String overrideLevel(String translateKey, Object[] parameters) {
return I18n.format(translateKey, parameters);
}
+ @Redirect(method = "updateActivePotionEffects", at = @At(value = "FIELD", target = "Lnet/minecraft/client/renderer/InventoryEffectRenderer;guiLeft:I", ordinal = 0))
+ public void shiftLeft(InventoryEffectRenderer instance, int value) {
+ if(TweaksMod.enabled && TweaksMod.instance.centredInventory) {
+ guiLeft = (width - xSize) / 2;
+ return;
+ }
+
+ guiLeft = value;
+ }
+
}
@Mixin(ItemPotion.class)
@@ -184,7 +200,7 @@ public void adjustRotation(float angle, float x, float y, float z) {
if(TweaksMod.enabled) {
angle *= TweaksMod.instance.getDamageShakeIntensity();
}
-
+
GlStateManager.rotate(angle, x, y, z);
}
diff --git a/game/src/main/java/io/github/solclient/client/mixin/mod/MixinV1_7VisualsMod.java b/game/src/main/java/io/github/solclient/client/mixin/mod/MixinV1_7VisualsMod.java
index a37d3f5d..a3773ea4 100755
--- a/game/src/main/java/io/github/solclient/client/mixin/mod/MixinV1_7VisualsMod.java
+++ b/game/src/main/java/io/github/solclient/client/mixin/mod/MixinV1_7VisualsMod.java
@@ -1,7 +1,5 @@
package io.github.solclient.client.mixin.mod;
-import org.objectweb.asm.MethodVisitor;
-import org.objectweb.asm.Opcodes;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
@@ -12,19 +10,15 @@
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import io.github.solclient.client.mod.impl.V1_7VisualsMod;
-import io.github.solclient.client.util.access.AccessEntityLivingBase;
import io.github.solclient.client.util.access.AccessMinecraft;
import net.minecraft.client.Minecraft;
import net.minecraft.client.entity.AbstractClientPlayer;
import net.minecraft.client.renderer.EntityRenderer;
-import net.minecraft.client.renderer.GlStateManager;
import net.minecraft.client.renderer.ItemRenderer;
import net.minecraft.client.renderer.entity.layers.LayerArmorBase;
import net.minecraft.entity.Entity;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.item.ItemStack;
-import net.minecraft.util.MathHelper;
-import net.minecraft.util.MovingObjectPosition;
public abstract class MixinV1_7VisualsMod {
diff --git a/game/src/main/java/io/github/solclient/client/mod/ModOption.java b/game/src/main/java/io/github/solclient/client/mod/ModOption.java
index 7c06fa1d..4ddcdb77 100644
--- a/game/src/main/java/io/github/solclient/client/mod/ModOption.java
+++ b/game/src/main/java/io/github/solclient/client/mod/ModOption.java
@@ -48,10 +48,6 @@ public ModOption(Mod mod, Option option, Field field) throws IOException {
this.mod = mod;
this.field = field;
- if(Modifier.isFinal(field.getModifiers()) && !(field.getType() == KeyBinding.class)) {
- LOGGER.warn("mod option " + field.getName() + " is final");
- }
-
if(field != null) {
field.setAccessible(true);
@@ -61,6 +57,16 @@ public ModOption(Mod mod, Option option, Field field) throws IOException {
file = new File(Minecraft.getMinecraft().mcDataDir, configFile.file());
readFile();
}
+
+ String name = field.getDeclaringClass() + "." + field.getName();
+
+ if(Modifier.isFinal(field.getModifiers()) && !(field.getType() == KeyBinding.class)) {
+ LOGGER.warn("Mod option {} is final", name);
+ }
+
+ if(getValue() == null) {
+ LOGGER.warn("Mod option {} has no default value. This may cause a crash.", name);
+ }
}
priority = option.priority();
diff --git a/game/src/main/java/io/github/solclient/client/mod/hud/SmoothCounterHudMod.java b/game/src/main/java/io/github/solclient/client/mod/hud/SmoothCounterHudMod.java
index 11bdb715..ddf918ae 100644
--- a/game/src/main/java/io/github/solclient/client/mod/hud/SmoothCounterHudMod.java
+++ b/game/src/main/java/io/github/solclient/client/mod/hud/SmoothCounterHudMod.java
@@ -1,11 +1,20 @@
package io.github.solclient.client.mod.hud;
+import com.google.gson.annotations.Expose;
+
import io.github.solclient.client.event.EventHandler;
import io.github.solclient.client.event.impl.PostTickEvent;
+import io.github.solclient.client.mod.annotation.AbstractTranslationKey;
+import io.github.solclient.client.mod.annotation.Option;
import net.minecraft.client.Minecraft;
+@AbstractTranslationKey("sol_client.mod.smooth_counter_hud")
public abstract class SmoothCounterHudMod extends SimpleHudMod {
+ @Expose
+ @Option
+ private boolean smoothNumbers = true;
+
public abstract int getIntValue();
public abstract String getSuffix();
@@ -14,7 +23,15 @@ public abstract class SmoothCounterHudMod extends SimpleHudMod {
@EventHandler
public void onTick(PostTickEvent event) {
+ if(mc.theWorld == null) return;
+
int actualValue = getIntValue();
+
+ if(!smoothNumbers) {
+ counter = actualValue;
+ return;
+ }
+
if(actualValue > counter) {
counter += Math.max(((actualValue - counter) / 2), 1);
}
diff --git a/game/src/main/java/io/github/solclient/client/mod/impl/SolClientMod.java b/game/src/main/java/io/github/solclient/client/mod/impl/SolClientMod.java
index 84fa1e06..d6283991 100644
--- a/game/src/main/java/io/github/solclient/client/mod/impl/SolClientMod.java
+++ b/game/src/main/java/io/github/solclient/client/mod/impl/SolClientMod.java
@@ -21,7 +21,7 @@ public class SolClientMod extends ConfigOnlyMod {
@Expose
@Option
- public boolean fancyMainMenu;
+ public boolean fancyMainMenu = true;
@Expose
@Option
diff --git a/game/src/main/java/io/github/solclient/client/mod/impl/TweaksMod.java b/game/src/main/java/io/github/solclient/client/mod/impl/TweaksMod.java
index f16c5d59..6b0782cf 100644
--- a/game/src/main/java/io/github/solclient/client/mod/impl/TweaksMod.java
+++ b/game/src/main/java/io/github/solclient/client/mod/impl/TweaksMod.java
@@ -56,6 +56,9 @@ public class TweaksMod extends Mod {
@Expose
@Option
private boolean borderlessFullscreen;
+ @Expose
+ @Option
+ public boolean centredInventory = true;
private Rectangle previousBounds;
private long fullscreenTime = -1;
diff --git a/game/src/main/java/io/github/solclient/client/mod/impl/hud/armour/ArmourMod.java b/game/src/main/java/io/github/solclient/client/mod/impl/hud/armour/ArmourMod.java
index f618a77e..5205b0c3 100644
--- a/game/src/main/java/io/github/solclient/client/mod/impl/hud/armour/ArmourMod.java
+++ b/game/src/main/java/io/github/solclient/client/mod/impl/hud/armour/ArmourMod.java
@@ -8,7 +8,8 @@
import io.github.solclient.client.util.data.Colour;
import io.github.solclient.client.util.data.Position;
import io.github.solclient.client.util.data.Rectangle;
-import net.minecraft.client.entity.EntityPlayerSP;
+import net.minecraft.client.gui.ScaledResolution;
+import net.minecraft.client.renderer.GlStateManager;
import net.minecraft.client.renderer.RenderHelper;
import net.minecraft.init.Items;
import net.minecraft.item.ItemStack;
@@ -31,6 +32,9 @@ public class ArmourMod extends HudMod {
@Option
private boolean hand = true;
@Expose
+ @Option
+ private boolean horizontal;
+ @Expose
@Option(translationKey = SimpleHudMod.TRANSLATION_KEY)
private boolean shadow = true;
@Expose
@@ -44,46 +48,124 @@ public String getId() {
@Override
public Rectangle getBounds(Position position) {
+ if(horizontal) {
+ int width = 1;
+ int unit = 16;
+
+ if(armour) {
+ switch(durability) {
+ case FRACTION:
+ width += 296;
+ break;
+ case REMAINING:
+ width += 168;
+ break;
+ case PERCENTAGE:
+ width += 192;
+ break;
+ default:
+ width += 73;
+ break;
+ }
+ }
+
+ if(hand) {
+ switch(durability) {
+ case FRACTION:
+ width += 85;
+ break;
+ case REMAINING:
+ width += 47;
+ break;
+ case PERCENTAGE:
+ width += 48;
+ break;
+ default:
+ width += 17;
+ break;
+ }
+ }
+
+ return new Rectangle(position.getX() - 1, position.getY(), width, 18);
+ }
int height = 1;
- if(armour) height += 15 * 4;
- if(hand) height += 15;
- return new Rectangle(position.getX() - 1, position.getY(), durability.getWidth() + 1, height);
+
+ if(armour) {
+ height += 15 * 4;
+ }
+
+ if(hand) {
+ height += 15;
+ }
+
+ return new Rectangle(position.getX() - 1, position.getY(), durability.getWidth() + 18, height);
}
@Override
public void render(Position position, boolean editMode) {
RenderHelper.enableGUIStandardItemLighting();
- if(mc.thePlayer != null && !editMode) {
- EntityPlayerSP player = mc.thePlayer;
- if(armour) {
- for(int i = 0; i < 4; i++) {
- ItemStack stack = player.inventory.armorInventory[3 - i];
- if(stack != null) {
- renderStack(stack, position.getX(), position.getY() + (i * 15));
+ ScaledResolution resolution = new ScaledResolution(mc);
+ boolean rtl = !horizontal && position.getX() > resolution.getScaledWidth() / 2;
+
+ int x = position.getX(), y = position.getY();
+
+ if(horizontal) {
+ y++;
+ }
+
+ ItemStack[] playerArmour;
+ ItemStack handItem;
+
+ if(mc.thePlayer == null || editMode) {
+ playerArmour = new ItemStack[] { BOOTS, LEGGINGS, CHESTPLATE, HELMET };
+ handItem = HAND;
+ }
+ else {
+ playerArmour = mc.thePlayer.inventory.armorInventory;
+ handItem = mc.thePlayer.inventory.getCurrentItem();
+ }
+
+ if(armour) {
+ for(int i = 0; i < 4; i++) {
+ ItemStack stack = playerArmour[3 - i];
+ if(stack != null) {
+ int width = renderStack(stack, x, y, rtl);
+ if(horizontal) {
+ if(width != 0) {
+ x += 24 + width;
+ }
+ else {
+ x += 18;
+ }
}
}
+ if(!horizontal) {
+ y += 15;
+ }
}
- if(hand && player.inventory.getCurrentItem() != null) renderStack(player.inventory.getCurrentItem(),
- position.getX(), position.getY() + (armour ? 60 : 0));
}
- else if(editMode) {
- if(armour) {
- renderStack(HELMET, position.getX(), position.getY());
- renderStack(CHESTPLATE, position.getX(), position.getY() + 15);
- renderStack(LEGGINGS, position.getX(), position.getY() + 30);
- renderStack(BOOTS, position.getX(), position.getY() + 45);
- }
- if(hand) renderStack(HAND, position.getX(), position.getY() + (armour ? 60 : 0));
+ if(hand && handItem != null) {
+ renderStack(handItem, x, y, rtl);
}
RenderHelper.disableStandardItemLighting();
}
- private void renderStack(ItemStack stack, int x, int y) {
- mc.getRenderItem().renderItemIntoGUI(stack, x, y);
- if(stack.getMaxDamage() > 0) {
+ private int renderStack(ItemStack stack, int x, int y, boolean rtl) {
+ boolean hasDurability = durability != DurabilityDisplay.OFF;
+ int itemX = x;
+
+ if(rtl && hasDurability) {
+ itemX += durability.getWidth() - 1;
+ }
+
+ mc.getRenderItem().renderItemIntoGUI(stack, itemX, y);
+ mc.getRenderItem().renderItemOverlays(font, stack, itemX, y);
+ GlStateManager.disableLighting();
+
+ if(hasDurability && stack.getMaxDamage() > 0) {
String text;
switch(durability) {
case FRACTION:
@@ -96,10 +178,23 @@ private void renderStack(ItemStack stack, int x, int y) {
text = ((int) (((double) stack.getMaxDamage() - stack.getItemDamage()) / (stack.getMaxDamage()) * 100)) + "%";
break;
default:
- text = "Invalid mode";
+ text = "??";
+ break;
+ }
+
+ if(rtl) {
+ x += durability.getWidth() - 4 - font.getStringWidth(text);
}
- font.drawString(text, x + 20, y + 5, textColour.getValue(), shadow);
+ else {
+ x += 20;
+ }
+
+ font.drawString(text, x, y + 4, textColour.getValue(), shadow);
+
+ return font.getStringWidth(text);
}
+
+ return 0;
}
}
diff --git a/game/src/main/java/io/github/solclient/client/mod/impl/hud/armour/DurabilityDisplay.java b/game/src/main/java/io/github/solclient/client/mod/impl/hud/armour/DurabilityDisplay.java
index 47cc8f64..ab364960 100644
--- a/game/src/main/java/io/github/solclient/client/mod/impl/hud/armour/DurabilityDisplay.java
+++ b/game/src/main/java/io/github/solclient/client/mod/impl/hud/armour/DurabilityDisplay.java
@@ -3,6 +3,7 @@
import net.minecraft.client.resources.I18n;
public enum DurabilityDisplay {
+ OFF,
FRACTION,
REMAINING,
PERCENTAGE;
@@ -14,14 +15,14 @@ public String toString() {
public int getWidth() {
switch(this) {
- case FRACTION:
- return 84;
- case REMAINING:
- return 46;
- case PERCENTAGE:
- return 47;
- default:
- return 0;
+ case FRACTION:
+ return 67;
+ case REMAINING:
+ return 29;
+ case PERCENTAGE:
+ return 30;
+ default:
+ return 0;
}
}
diff --git a/game/src/main/java/io/github/solclient/client/mod/impl/hud/PingMod.java b/game/src/main/java/io/github/solclient/client/mod/impl/hud/ping/PingMod.java
similarity index 65%
rename from game/src/main/java/io/github/solclient/client/mod/impl/hud/PingMod.java
rename to game/src/main/java/io/github/solclient/client/mod/impl/hud/ping/PingMod.java
index fb3beeb2..2f9306d8 100755
--- a/game/src/main/java/io/github/solclient/client/mod/impl/hud/PingMod.java
+++ b/game/src/main/java/io/github/solclient/client/mod/impl/hud/ping/PingMod.java
@@ -1,14 +1,18 @@
-package io.github.solclient.client.mod.impl.hud;
+package io.github.solclient.client.mod.impl.hud.ping;
import java.net.UnknownHostException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
+import com.google.gson.annotations.Expose;
+
import io.github.solclient.client.event.EventHandler;
import io.github.solclient.client.event.impl.PostTickEvent;
import io.github.solclient.client.event.impl.ServerConnectEvent;
+import io.github.solclient.client.mod.annotation.Option;
import io.github.solclient.client.mod.hud.SmoothCounterHudMod;
import io.github.solclient.client.util.Utils;
+import net.minecraft.client.network.NetworkPlayerInfo;
public class PingMod extends SmoothCounterHudMod {
@@ -16,6 +20,10 @@ public class PingMod extends SmoothCounterHudMod {
private static final int PING_INTERVAL = 600;
private int nextPing;
+ @Expose
+ @Option
+ private PingSource source = PingSource.AUTO;
+
@Override
public String getId() {
return "ping";
@@ -29,6 +37,10 @@ public void onServerConnect(ServerConnectEvent event) {
@EventHandler
public void updatePing(PostTickEvent event) {
+ if(source.resolve() != PingSource.MULTIPLAYER_SCREEN) {
+ return;
+ }
+
if(mc.getCurrentServerData() != null && !mc.isIntegratedServerRunning()) {
if(nextPing > 0) {
nextPing--;
@@ -50,7 +62,7 @@ else if(nextPing > -1) {
});
}
catch(UnknownHostException error) {
- error.printStackTrace();
+ logger.error("Could not ping server", error);
}
nextPing = PING_INTERVAL;
@@ -61,10 +73,18 @@ else if(nextPing > -1) {
@Override
public int getIntValue() {
- if(ping != -1) {
+ if(ping != -1 && source.resolve() == PingSource.MULTIPLAYER_SCREEN) {
return ping;
}
+ if(source.resolve() == PingSource.TAB_LIST && !mc.isIntegratedServerRunning() && mc.thePlayer != null
+ && mc.getCurrentServerData() != null) {
+ NetworkPlayerInfo info = mc.thePlayer.sendQueue.getPlayerInfo(mc.getSession().getProfile().getId());
+ if(info != null) {
+ return info.getResponseTime();
+ }
+ }
+
return 0;
}
diff --git a/game/src/main/java/io/github/solclient/client/mod/impl/hud/ping/PingSource.java b/game/src/main/java/io/github/solclient/client/mod/impl/hud/ping/PingSource.java
new file mode 100644
index 00000000..2893cca9
--- /dev/null
+++ b/game/src/main/java/io/github/solclient/client/mod/impl/hud/ping/PingSource.java
@@ -0,0 +1,29 @@
+package io.github.solclient.client.mod.impl.hud.ping;
+
+import io.github.solclient.client.Client;
+import io.github.solclient.client.DetectedServer;
+import net.minecraft.client.resources.I18n;
+
+public enum PingSource {
+ AUTO,
+ MULTIPLAYER_SCREEN,
+ TAB_LIST;
+
+ public PingSource resolve() {
+ if(this == AUTO) {
+ if(Client.INSTANCE.detectedServer == DetectedServer.HYPIXEL) {
+ return MULTIPLAYER_SCREEN;
+ }
+
+ return TAB_LIST;
+ }
+
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ return I18n.format("sol_client.mod.ping.option.source." + name().toLowerCase());
+ }
+
+}
diff --git a/game/src/main/java/io/github/solclient/client/mod/impl/hud/SpeedometerMod.java b/game/src/main/java/io/github/solclient/client/mod/impl/hud/speedometer/SpeedometerMod.java
similarity index 81%
rename from game/src/main/java/io/github/solclient/client/mod/impl/hud/SpeedometerMod.java
rename to game/src/main/java/io/github/solclient/client/mod/impl/hud/speedometer/SpeedometerMod.java
index c21847e2..64699e96 100755
--- a/game/src/main/java/io/github/solclient/client/mod/impl/hud/SpeedometerMod.java
+++ b/game/src/main/java/io/github/solclient/client/mod/impl/hud/speedometer/SpeedometerMod.java
@@ -1,4 +1,4 @@
-package io.github.solclient.client.mod.impl.hud;
+package io.github.solclient.client.mod.impl.hud.speedometer;
import java.text.DecimalFormat;
import java.util.Arrays;
@@ -100,10 +100,9 @@ public void render(Position position, boolean editMode) {
}
private double getSpeed() {
- double distTraveledLastTickX = mc.thePlayer.posX - mc.thePlayer.prevPosX;
- double distTraveledLastTickZ = mc.thePlayer.posZ - mc.thePlayer.prevPosZ;
- return MathHelper.sqrt_double(distTraveledLastTickX * distTraveledLastTickX
- + distTraveledLastTickZ * distTraveledLastTickZ);
+ double xTraveled = mc.thePlayer.posX - mc.thePlayer.prevPosX;
+ double zTraveled = mc.thePlayer.posZ - mc.thePlayer.prevPosZ;
+ return MathHelper.sqrt_double(xTraveled * xTraveled + zTraveled * zTraveled);
}
@Override
@@ -116,11 +115,7 @@ public String getText(boolean editMode) {
return "0.00 m/s";
}
else {
- double distTraveledLastTickX = mc.thePlayer.posX - mc.thePlayer.prevPosX;
- double distTraveledLastTickZ = mc.thePlayer.posZ - mc.thePlayer.prevPosZ;
- double currentSpeed = MathHelper.sqrt_double(distTraveledLastTickX * distTraveledLastTickX
- + distTraveledLastTickZ * distTraveledLastTickZ);
- return FORMAT.format(currentSpeed / 0.05F) + " m/s";
+ return FORMAT.format(getSpeed() / 0.05F) + " m/s";
}
}
diff --git a/game/src/main/java/io/github/solclient/client/mod/impl/quickplay/QuickPlayMod.java b/game/src/main/java/io/github/solclient/client/mod/impl/quickplay/QuickPlayMod.java
index 4e30fb2b..fa8ef73e 100644
--- a/game/src/main/java/io/github/solclient/client/mod/impl/quickplay/QuickPlayMod.java
+++ b/game/src/main/java/io/github/solclient/client/mod/impl/quickplay/QuickPlayMod.java
@@ -28,6 +28,7 @@ public class QuickPlayMod extends Mod {
@Option
private final KeyBinding menuKey = new KeyBinding(getTranslationKey() + ".key", Keyboard.KEY_M, Client.KEY_CATEGORY);
+ private boolean got;
private QuickPlayDatabase database;
@Expose
private final List recentlyPlayed = new ArrayList<>();
@@ -35,10 +36,6 @@ public class QuickPlayMod extends Mod {
@Override
public void onRegister() {
super.onRegister();
-
- Utils.MAIN_EXECUTOR.submit(() -> {
- database = new QuickPlayDatabase();
- });
Client.INSTANCE.registerKeyBinding(menuKey);
}
@@ -74,6 +71,17 @@ public void onTick(PreTickEvent event) {
}
}
+ @Override
+ protected void onEnable() {
+ super.onEnable();
+ if(!got) {
+ got = true;
+ Utils.MAIN_EXECUTOR.submit(() -> {
+ database = new QuickPlayDatabase();
+ });
+ }
+ }
+
@Override
public boolean isEnabledByDefault() {
return true;
diff --git a/game/src/main/java/io/github/solclient/client/mod/impl/quickplay/ui/QuickPlayPalette.java b/game/src/main/java/io/github/solclient/client/mod/impl/quickplay/ui/QuickPlayPalette.java
index 489d4416..d00cffee 100644
--- a/game/src/main/java/io/github/solclient/client/mod/impl/quickplay/ui/QuickPlayPalette.java
+++ b/game/src/main/java/io/github/solclient/client/mod/impl/quickplay/ui/QuickPlayPalette.java
@@ -185,18 +185,20 @@ private void clampIndex() {
protected void keyTyped(char typedChar, int keyCode) throws IOException {
super.keyTyped(typedChar, keyCode);
- if(typedChar > 31 && typedChar != '§') {
+ if(keyCode == Keyboard.KEY_BACK) {
+ if(!query.isEmpty()) {
+ if(GuiScreen.isCtrlKeyDown()) {
+ query = "";
+ }
+ else {
+ query = query.substring(0, query.length() - 1);
+ }
+ }
+ }
+ else if(typedChar > 31 && typedChar != '§') {
query += typedChar;
inAllGames = false;
}
- else if(typedChar == 8 && !query.isEmpty()) {
- if(GuiScreen.isCtrlKeyDown()) {
- query = "";
- }
- else {
- query = query.substring(0, query.length() - 1);
- }
- }
if(keyCode == Keyboard.KEY_DOWN) {
selectedIndex++;
diff --git a/game/src/main/java/io/github/solclient/client/tweak/Tweaker.java b/game/src/main/java/io/github/solclient/client/tweak/Tweaker.java
index fe7df2fb..0a9a1372 100755
--- a/game/src/main/java/io/github/solclient/client/tweak/Tweaker.java
+++ b/game/src/main/java/io/github/solclient/client/tweak/Tweaker.java
@@ -94,7 +94,7 @@ private void getExceptions() {
@Override
public String getLaunchTarget() {
- return "net.minecraft.client.main.Main";
+ return "io.github.solclient.client.PreMain";
}
@Override
diff --git a/game/src/main/java/io/github/solclient/client/tweak/transformer/ClassTransformer.java b/game/src/main/java/io/github/solclient/client/tweak/transformer/ClassTransformer.java
index b4a7f45c..2714a370 100644
--- a/game/src/main/java/io/github/solclient/client/tweak/transformer/ClassTransformer.java
+++ b/game/src/main/java/io/github/solclient/client/tweak/transformer/ClassTransformer.java
@@ -1,18 +1,17 @@
package io.github.solclient.client.tweak.transformer;
-import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
-import org.apache.commons.io.FileUtils;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.tree.ClassNode;
import io.github.solclient.client.tweak.transformer.impl.TransformerGuiButton;
import io.github.solclient.client.tweak.transformer.impl.TransformerGuiScreen;
+import io.github.solclient.client.tweak.transformer.impl.TransformerLinuxKeycodes;
import io.github.solclient.client.tweak.transformer.impl.TransformerMinecraft;
import io.github.solclient.client.tweak.transformer.impl.TransformerWorldClient;
import net.minecraft.launchwrapper.IClassTransformer;
@@ -26,6 +25,7 @@ public ClassTransformer() {
register(new TransformerGuiScreen());
register(new TransformerWorldClient());
register(new TransformerMinecraft());
+ register(new TransformerLinuxKeycodes());
}
@Override
diff --git a/game/src/main/java/io/github/solclient/client/tweak/transformer/impl/TransformerGuiScreen.java b/game/src/main/java/io/github/solclient/client/tweak/transformer/impl/TransformerGuiScreen.java
index a4050517..c57e056c 100644
--- a/game/src/main/java/io/github/solclient/client/tweak/transformer/impl/TransformerGuiScreen.java
+++ b/game/src/main/java/io/github/solclient/client/tweak/transformer/impl/TransformerGuiScreen.java
@@ -1,13 +1,12 @@
package io.github.solclient.client.tweak.transformer.impl;
+import java.lang.reflect.Modifier;
+
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldNode;
-import org.objectweb.asm.tree.MethodNode;
import io.github.solclient.client.tweak.transformer.ClassNodeTransformer;
-import java.lang.reflect.Modifier;
-
public class TransformerGuiScreen implements ClassNodeTransformer {
@Override
diff --git a/game/src/main/java/io/github/solclient/client/tweak/transformer/impl/TransformerLinuxKeycodes.java b/game/src/main/java/io/github/solclient/client/tweak/transformer/impl/TransformerLinuxKeycodes.java
new file mode 100644
index 00000000..b69d1d8b
--- /dev/null
+++ b/game/src/main/java/io/github/solclient/client/tweak/transformer/impl/TransformerLinuxKeycodes.java
@@ -0,0 +1,21 @@
+package io.github.solclient.client.tweak.transformer.impl;
+
+import java.lang.reflect.Modifier;
+
+import org.objectweb.asm.tree.ClassNode;
+
+import io.github.solclient.client.tweak.transformer.ClassNodeTransformer;
+
+public class TransformerLinuxKeycodes implements ClassNodeTransformer {
+
+ @Override
+ public boolean test(String name) {
+ return name.equals("org/lwjgl/opengl/LinuxKeycodes");
+ }
+
+ @Override
+ public void apply(ClassNode clazz) {
+ clazz.access |= Modifier.PUBLIC;
+ }
+
+}
diff --git a/game/src/main/java/io/github/solclient/client/ui/component/ComponentRenderInfo.java b/game/src/main/java/io/github/solclient/client/ui/component/ComponentRenderInfo.java
index 96814516..70a89b99 100644
--- a/game/src/main/java/io/github/solclient/client/ui/component/ComponentRenderInfo.java
+++ b/game/src/main/java/io/github/solclient/client/ui/component/ComponentRenderInfo.java
@@ -5,8 +5,7 @@
@Data
public class ComponentRenderInfo {
- private final int relativeMouseX;
- private final int relativeMouseY;
+ private final int relativeMouseX, relativeMouseY;
private final float partialTicks;
}
diff --git a/game/src/main/java/io/github/solclient/client/ui/component/impl/ScrollListComponent.java b/game/src/main/java/io/github/solclient/client/ui/component/impl/ScrollListComponent.java
index 11558a93..5ff1ac16 100644
--- a/game/src/main/java/io/github/solclient/client/ui/component/impl/ScrollListComponent.java
+++ b/game/src/main/java/io/github/solclient/client/ui/component/impl/ScrollListComponent.java
@@ -1,11 +1,13 @@
package io.github.solclient.client.ui.component.impl;
import org.lwjgl.input.Keyboard;
+import org.lwjgl.input.Mouse;
import io.github.solclient.client.mod.impl.SolClientMod;
import io.github.solclient.client.ui.component.Component;
import io.github.solclient.client.ui.component.ComponentRenderInfo;
import io.github.solclient.client.ui.component.controller.AlignedBoundsController;
+import io.github.solclient.client.ui.component.controller.AnimatedColourController;
import io.github.solclient.client.ui.screen.mods.ModListing;
import io.github.solclient.client.util.data.Alignment;
import io.github.solclient.client.util.data.Colour;
@@ -21,12 +23,17 @@ public abstract class ScrollListComponent extends Component {
private double calculatedY;
private int maxScrolling;
private double scrollPercent;
+ private Component scrollbar;
+ private int scrolling = -1;
+ private int lastMouseY;
+ private double grabStartY = -1;
+ private int grabMouseY;
@Override
public void setParent(Component parent) {
super.setParent(parent);
- parent.add(new BlockComponent(Colour.LIGHT_BUTTON) {
+ parent.add(scrollbar = new BlockComponent(Colour.LIGHT_BUTTON) {
@Override
public void render(ComponentRenderInfo info) {
@@ -40,7 +47,7 @@ public void render(ComponentRenderInfo info) {
}
}, (component, defaultBounds) -> {
- return new Rectangle(getBounds().getX() + getBounds().getWidth() - 5, getBounds().getY(), 3,
+ return new Rectangle(getBounds().getX() + getBounds().getWidth() - 8, getBounds().getY(), 5,
(int) (getBounds().getHeight() * scrollPercent));
});
}
@@ -88,9 +95,75 @@ public void render(ComponentRenderInfo info) {
maxScrolling = 0;
}
+ if(lastMouseY != info.getRelativeMouseY()) {
+ if(scrolling > 0) {
+ int targetCompY = info.getRelativeMouseY() - scrolling;
+ jumpTo((int) (targetCompY / (getBounds().getHeight() / (double) getContentHeight())));
+ clamp();
+ }
+ else if(grabStartY != -1) {
+ jumpTo(grabStartY - (info.getRelativeMouseY() - grabMouseY));
+ clamp();
+ }
+ }
+
+ lastMouseY = info.getRelativeMouseY();
+
super.render(translate(info));
}
+ private int mouseInScrollbar(ComponentRenderInfo info) {
+ Rectangle scrollBounds = scrollbar.getBounds().offset(-getBounds().getX(), -getBounds().getY() + (int) (calculatedY * scrollPercent));
+
+ if(new Rectangle(scrollBounds.getX(), 0, scrollBounds.getWidth(),
+ getBounds().getHeight()).contains(info.getRelativeMouseX(), info.getRelativeMouseY())) {
+ if(!scrollBounds.contains(info.getRelativeMouseX(), info.getRelativeMouseY())) {
+ return scrollbar.getBounds().getHeight() / 2;
+ }
+
+ return info.getRelativeMouseY() - scrollBounds.getY();
+ }
+
+ return -1;
+ }
+
+ @Override
+ public boolean mouseClicked(ComponentRenderInfo info, int button) {
+ if(button == 0) {
+ scrolling = mouseInScrollbar(reverseTranslation(info));
+ lastMouseY = -1;
+ if(scrolling != -1) {
+ return true;
+ }
+ }
+
+ boolean superResult = super.mouseClicked(info, button);
+
+
+ if(button == 0 && !superResult && grabStartY == -1) {
+ grabStartY = targetY;
+ grabMouseY = reverseTranslation(info).getRelativeMouseY();
+ }
+
+ return true;
+ }
+
+ @Override
+ public boolean mouseReleasedAnywhere(ComponentRenderInfo info, int button, boolean inside) {
+ if(button == 0) {
+ if(scrolling != -1) {
+ scrolling = -1;
+ return true;
+ }
+ if(grabStartY != -1) {
+ grabStartY = -1;
+ return true;
+ }
+ }
+
+ return super.mouseReleasedAnywhere(info, button, inside);
+ }
+
@Override
protected boolean shouldCull(Component component) {
if((component.getBounds().getEndY() - calculatedY) < 0) {
@@ -107,7 +180,7 @@ public void snapTo(int scroll) {
targetY = animatedY = lastAnimatedY = calculatedY = maxScrolling = scroll;
}
- public void jumpTo(int scroll) {
+ public void jumpTo(double scroll) {
targetY = scroll;
}
diff --git a/game/src/main/java/io/github/solclient/client/ui/screen/mods/ModListing.java b/game/src/main/java/io/github/solclient/client/ui/screen/mods/ModListing.java
index d22151df..4ab52d78 100644
--- a/game/src/main/java/io/github/solclient/client/ui/screen/mods/ModListing.java
+++ b/game/src/main/java/io/github/solclient/client/ui/screen/mods/ModListing.java
@@ -97,9 +97,11 @@ protected Rectangle getDefaultBounds() {
@Override
public boolean mouseClicked(ComponentRenderInfo info, int button) {
if(button == 0 || (!mod.isBlocked() && (button == 0 || button == 1))) {
+ Utils.playClickSound(true);
+
if(mod.isBlocked()) {
if(Client.INSTANCE.detectedServer == null) {
- return false;
+ return true;
}
URI blockedModPage = Client.INSTANCE.detectedServer.getBlockedModPage();
@@ -108,11 +110,9 @@ public boolean mouseClicked(ComponentRenderInfo info, int button) {
Utils.openUrl(blockedModPage.toString());
}
- return false;
+ return true;
}
- Utils.playClickSound(true);
-
if(settingsButton.isHovered() || mod.isLocked() || button == 1) {
screen.switchMod(mod);
return true;
diff --git a/game/src/main/java/io/github/solclient/client/util/Utils.java b/game/src/main/java/io/github/solclient/client/util/Utils.java
index 69258283..c59b8003 100755
--- a/game/src/main/java/io/github/solclient/client/util/Utils.java
+++ b/game/src/main/java/io/github/solclient/client/util/Utils.java
@@ -6,33 +6,29 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
-import java.io.OutputStream;
import java.io.PrintStream;
-import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
-import java.net.HttpURLConnection;
import java.net.InetAddress;
+import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
+import java.net.URLDecoder;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
-import java.nio.file.Paths;
import java.util.Comparator;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadLocalRandom;
import java.util.function.IntConsumer;
-import org.apache.commons.io.IOUtils;
import org.lwjgl.opengl.GL11;
-import com.google.gson.JsonObject;
-import com.google.gson.JsonParser;
import com.replaymod.replay.ReplayModReplay;
import com.replaymod.replay.camera.CameraEntity;
+import com.replaymod.replaystudio.lib.viaversion.libs.kyori.adventure.text.event.ClickEvent.Action;
-import io.github.solclient.client.lib.penner.easing.Linear;
+import io.github.solclient.client.Client;
import io.github.solclient.client.mod.impl.SolClientMod;
import io.github.solclient.client.util.data.Colour;
import io.github.solclient.client.util.data.Rectangle;
@@ -41,6 +37,7 @@
import net.minecraft.client.Minecraft;
import net.minecraft.client.audio.PositionedSoundRecord;
import net.minecraft.client.gui.GuiChat;
+import net.minecraft.client.gui.GuiIngame;
import net.minecraft.client.gui.GuiScreen;
import net.minecraft.client.gui.ScaledResolution;
import net.minecraft.client.multiplayer.ServerAddress;
@@ -48,6 +45,8 @@
import net.minecraft.client.renderer.Tessellator;
import net.minecraft.client.renderer.WorldRenderer;
import net.minecraft.client.renderer.vertex.DefaultVertexFormats;
+import net.minecraft.event.ClickEvent;
+import net.minecraft.launchwrapper.Launch;
import net.minecraft.network.EnumConnectionState;
import net.minecraft.network.NetworkManager;
import net.minecraft.network.handshake.client.C00Handshake;
@@ -59,6 +58,7 @@
import net.minecraft.scoreboard.ScoreObjective;
import net.minecraft.util.AxisAlignedBB;
import net.minecraft.util.ChatComponentText;
+import net.minecraft.util.ChatStyle;
import net.minecraft.util.EnumChatFormatting;
import net.minecraft.util.IChatComponent;
import net.minecraft.util.MathHelper;
@@ -69,19 +69,10 @@
@UtilityClass
public class Utils {
- private PrintStream out;
+ public static final String REVEAL_SUFFIX = "§sol_client:showinfolder";
public final ExecutorService MAIN_EXECUTOR = Executors.newFixedThreadPool(Math.max(Runtime.getRuntime().availableProcessors(), 2));
public final Comparator STRING_WIDTH_COMPARATOR = Comparator.comparingInt(Utils::getStringWidth);
- static {
- try {
- out = new PrintStream(System.out, true, "UTF-8");
- }
- catch(UnsupportedEncodingException error) {
- out = System.out;
- }
- }
-
private int getStringWidth(String text) {
return Minecraft.getMinecraft().fontRendererObj.getStringWidth(text);
}
@@ -131,10 +122,10 @@ public void drawRectangle(double x, double y, double right, double bottom, int c
bottom = swap;
}
- float r = (float) (colour >> 24 & 255) / 255.0F;
- float g = (float) (colour >> 16 & 255) / 255.0F;
- float b = (float) (colour >> 8 & 255) / 255.0F;
- float a = (float) (colour & 255) / 255.0F;
+ float r = (colour >> 24 & 255) / 255.0F;
+ float g = (colour >> 16 & 255) / 255.0F;
+ float b = (colour >> 8 & 255) / 255.0F;
+ float a = (colour & 255) / 255.0F;
Tessellator tessellator = Tessellator.getInstance();
WorldRenderer worldrenderer = tessellator.getWorldRenderer();
@@ -216,53 +207,6 @@ public URL sneakyParse(String url) {
return new URL(url);
}
- /*
- * Single following method:
- *
- * Copyright (C) 2018-present Hyperium
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Lesser General Public License as published
- * by the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program. If not, see .
- */
- public void drawCircle(float xx, float yy, int radius, int col) {
- float f = (col >> 24 & 0xFF) / 255.0F;
- float f2 = (col >> 16 & 0xFF) / 255.0F;
- float f3 = (col >> 8 & 0xFF) / 255.0F;
- float f4 = (col & 0xFF) / 255.0F;
- GL11.glPushMatrix();
- GL11.glEnable(GL11.GL_BLEND);
- GL11.glDisable(GL11.GL_TEXTURE_2D);
- GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
- GL11.glEnable(GL11.GL_LINE_SMOOTH);
- GL11.glShadeModel(GL11.GL_SMOOTH);
- GL11.glLineWidth(2);
- GL11.glBegin(2);
- GlStateManager.color(f2, f3, f4, f);
-
- for (int i = 0; i < 70; i++) {
- float x = radius * MathHelper.cos((float) (i * 0.08975979010256552D));
- float y = radius * MathHelper.sin((float) (i * 0.08975979010256552D));
- GL11.glVertex2f(xx + x, yy + y);
- }
-
- GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F);
- GL11.glEnd();
- GL11.glEnable(GL11.GL_TEXTURE_2D);
- GL11.glDisable(GL11.GL_BLEND);
- GL11.glDisable(GL11.GL_LINE_SMOOTH);
- GL11.glPopMatrix();
- }
-
public GuiChat getChatGui() {
GuiScreen currentScreen = Minecraft.getMinecraft().currentScreen;
if(currentScreen != null && currentScreen instanceof GuiChat) {
@@ -368,7 +312,7 @@ public void handleServerInfo(S00PacketServerInfo packetIn) {
else {
expected = true;
time = Minecraft.getSystemTime();
- networkManager.sendPacket(new C01PacketPing(this.time));
+ networkManager.sendPacket(new C01PacketPing(time));
}
}
@@ -394,12 +338,139 @@ public int randomInt(int from, int to) {
return ThreadLocalRandom.current().nextInt(from, to + 1); // https://stackoverflow.com/a/363692
}
- public void openUrl(String url) {
- sendLauncherMessage("openUrl", url);
+ private String decodeUrl(String url) {
+ try {
+ return URLDecoder.decode(url, "UTF-8");
+ }
+ catch(UnsupportedEncodingException error) {
+ // UTF-8 is required
+ throw new Error(error);
+ }
+ }
+
+ private String urlDirname(String url) {
+ int lastSlash = url.lastIndexOf('/');
+ int lastBacklash = url.lastIndexOf('\\');
+
+ if(lastBacklash > lastSlash) {
+ lastSlash = lastBacklash;
+ }
+
+ return url.substring(0, lastSlash);
}
- private void sendLauncherMessage(String type, String... arguments) {
- out.println("message " + System.getProperty("io.github.solclient.client.secret") + " " + type + " " + String.join(" ", arguments));
+ public void openUrl(String url) {
+ String[] command;
+ boolean reveal = false;
+
+ // ensure that the url starts with file:/// as opposed to file://
+ if(url.startsWith("file:")) {
+ url = url.replace("file:", "file://");
+ url = url.substring(0, url.indexOf('/')) + '/' + url.substring(url.indexOf('/'));
+
+ if(url.endsWith(REVEAL_SUFFIX)) {
+ url = url.substring(0, url.length() - REVEAL_SUFFIX.length());
+ reveal = true;
+ }
+ }
+
+ switch(Util.getOSType()) {
+ case LINUX:
+ if(reveal) {
+ if(new File("/usr/bin/xdg-mime").exists() && new File("/usr/bin/gio").exists()) {
+ try {
+ Process process = new ProcessBuilder("xdg-mime", "query", "default", "inode/directory").start();
+ int code = process.waitFor();
+
+ if(code > 0) {
+ throw new IllegalStateException("xdg-mime exited with code " + code);
+ }
+
+ String file;
+ try(BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
+ file = reader.readLine();
+ }
+
+ if(file != null) {
+ url = decodeUrl(url);
+ url = url.substring(7);
+
+ if(!file.startsWith("/")) {
+ file = "/usr/share/applications/" + file;
+ }
+
+ command = new String[] { "gio", "launch", file, url };
+ break;
+ }
+ }
+ catch(IOException | InterruptedException | IllegalStateException error) {
+ Client.LOGGER.error("Could not determine directory handler:", error);
+ }
+ }
+ url = urlDirname(url);
+ }
+
+ if(new File("/usr/bin/xdg-open").exists()) {
+ command = new String[] { "xdg-open", url };
+ break;
+ }
+ // fall through to default
+ default:
+ // fall back to AWT, but without a message
+ command = null;
+ break;
+ case OSX:
+ if(reveal) {
+ command = new String[] { "open", "-R", decodeUrl(url).substring(7) };
+ }
+ else {
+ command = new String[] { "open", url };
+ }
+ break;
+ case WINDOWS:
+ if(reveal) {
+ command = new String[] { "Explorer", "/select," + decodeUrl(url).substring(8).replace('/', '\\') };
+ }
+ else {
+ command = new String[] { "rundll32", "url.dll,FileProtocolHandler", url };
+ }
+
+ break;
+ }
+
+ if(command != null) {
+ try {
+ Process proc = new ProcessBuilder(command).start();
+ proc.getInputStream().close();
+ proc.getErrorStream().close();
+ proc.getOutputStream().close();
+ return;
+ }
+ catch(IOException error) {
+ Client.LOGGER.warn("Could not execute " + String.join(" ", command) + " - falling back to AWT:", error);
+ }
+ }
+
+ try {
+ Desktop.getDesktop().browse(URI.create(url));
+ }
+ catch(IOException error) {
+ Client.LOGGER.error("Could not open " + url + " with AWT:", error);
+
+ // null checks in case a link is opened before Minecraft is fully initialised
+
+ Minecraft mc = Minecraft.getMinecraft();
+ if(mc == null) {
+ return;
+ }
+
+ GuiIngame gui = mc.ingameGUI;
+ if(gui == null) {
+ return;
+ }
+
+ gui.getChatGUI().printChatMessage(new ChatComponentText("§cCould not open " + url + ". Please open it manually."));
+ }
}
public String getRelativeToPackFolder(File packFile) {
@@ -433,10 +504,10 @@ public void drawFloatRectangle(float left, float top, float right, float bottom,
bottom = swap;
}
- float f3 = (float) (colour >> 24 & 255) / 255.0F;
- float f = (float) (colour >> 16 & 255) / 255.0F;
- float f1 = (float) (colour >> 8 & 255) / 255.0F;
- float f2 = (float) (colour & 255) / 255.0F;
+ float f3 = (colour >> 24 & 255) / 255.0F;
+ float f = (colour >> 16 & 255) / 255.0F;
+ float f1 = (colour >> 8 & 255) / 255.0F;
+ float f2 = (colour & 255) / 255.0F;
Tessellator tessellator = Tessellator.getInstance();
WorldRenderer worldrenderer = tessellator.getWorldRenderer();
GlStateManager.enableBlend();
@@ -444,10 +515,10 @@ public void drawFloatRectangle(float left, float top, float right, float bottom,
GlStateManager.tryBlendFuncSeparate(770, 771, 1, 0);
GlStateManager.color(f, f1, f2, f3);
worldrenderer.begin(7, DefaultVertexFormats.POSITION);
- worldrenderer.pos((double) left, (double) bottom, 0.0D).endVertex();
- worldrenderer.pos((double) right, (double) bottom, 0.0D).endVertex();
- worldrenderer.pos((double) right, (double) top, 0.0D).endVertex();
- worldrenderer.pos((double) left, (double) top, 0.0D).endVertex();
+ worldrenderer.pos(left, bottom, 0.0D).endVertex();
+ worldrenderer.pos(right, bottom, 0.0D).endVertex();
+ worldrenderer.pos(right, top, 0.0D).endVertex();
+ worldrenderer.pos(left, top, 0.0D).endVertex();
tessellator.draw();
GlStateManager.enableTexture2D();
GlStateManager.disableBlend();
diff --git a/game/src/main/java/io/github/solclient/client/util/font/SlickFontRenderer.java b/game/src/main/java/io/github/solclient/client/util/font/SlickFontRenderer.java
index 29cc80ff..a4c691bd 100644
--- a/game/src/main/java/io/github/solclient/client/util/font/SlickFontRenderer.java
+++ b/game/src/main/java/io/github/solclient/client/util/font/SlickFontRenderer.java
@@ -31,11 +31,15 @@
import java.util.List;
import java.util.regex.Pattern;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
import org.lwjgl.opengl.GL11;
import org.newdawn.slick.SlickException;
import org.newdawn.slick.UnicodeFont;
import org.newdawn.slick.font.effects.ColorEffect;
+import org.newdawn.slick.font.effects.Effect;
+import io.github.solclient.client.mod.impl.SolClientMod;
import io.github.solclient.client.util.data.Colour;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.ScaledResolution;
@@ -44,25 +48,12 @@
import net.minecraft.util.StringUtils;
public class SlickFontRenderer implements Font {
+
+ private static final Logger LOGGER = LogManager.getLogger();
+
private static final Pattern COLOR_CODE_PATTERN = Pattern.compile("§[0123456789abcdefklmnor]");
- private final int[] colorCodes = {
- 0x000000,
- 0x0000AA,
- 0x00AA00,
- 0x00AAAA,
- 0xAA0000,
- 0xAA00AA,
- 0xFFAA00,
- 0xAAAAAA,
- 0x555555,
- 0x5555FF,
- 0x55FF55,
- 0x55FFFF,
- 0xFF5555,
- 0xFF55FF,
- 0xFFFF55,
- 0xFFFFFF
- };
+ private final int[] colorCodes = { 0x000000, 0x0000AA, 0x00AA00, 0x00AAAA, 0xAA0000, 0xAA00AA, 0xFFAA00, 0xAAAAAA,
+ 0x555555, 0x5555FF, 0x55FF55, 0x55FFFF, 0xFF5555, 0xFF55FF, 0xFFFF55, 0xFFFFFF };
private float scaleFactor;
private UnicodeFont slickFont;
private int prevScaleFactor;
@@ -83,7 +74,9 @@ private java.awt.Font getFontFromInput(String path) throws IOException, FontForm
@Override
public int renderString(String text, float x, float y, int colour) {
- if(text == null) return 0;
+ if(text == null || slickFont == null) {
+ return 0;
+ }
x = (int) x;
y = (int) y;
@@ -135,8 +128,7 @@ public int renderString(String text, float x, float y, int colour) {
char colorCode = characters[index];
if(colorCode == '§') {
char colorChar = characters[index + 1];
- int codeIndex = ("0123456789" +
- "abcdef").indexOf(colorChar);
+ int codeIndex = ("0123456789" + "abcdef").indexOf(colorChar);
if(codeIndex < 0) {
if(colorChar == 'r') {
currentColour = colour;
@@ -156,22 +148,56 @@ public int renderString(String text, float x, float y, int colour) {
return (int) x;
}
+ @SuppressWarnings("unchecked")
private void loadFont() {
ScaledResolution resolution = new ScaledResolution(Minecraft.getMinecraft());
scaleFactor = resolution.getScaleFactor();
if(scaleFactor != prevScaleFactor) {
+ if(slickFont != null) {
+ free();
+ }
+
try {
prevScaleFactor = resolution.getScaleFactor();
slickFont = new UnicodeFont(getFontFromInput(name).deriveFont(size * prevScaleFactor / 2));
slickFont.addAsciiGlyphs();
slickFont.addNeheGlyphs();
+
+ // Cyrillic
slickFont.addGlyphs(0x0400, 0x04FF);
- slickFont.getEffects().add(new ColorEffect(Colour.WHITE.toAWT()));
+
+ // Vietnamese
+ slickFont.addGlyphs(0x0041, 0x005A);
+ slickFont.addGlyphs(0x0061, 0x007A);
+ slickFont.addGlyphs(0x00C0, 0x00C3);
+ slickFont.addGlyphs(0x00C8, 0x00CA);
+ slickFont.addGlyphs(0x00CC, 0x00CD);
+ slickFont.addGlyphs(0x00D2, 0x00D5);
+ slickFont.addGlyphs(0x00D9, 0x00DA);
+ slickFont.addGlyphs(0x00DD, 0x00DD);
+ slickFont.addGlyphs(0x00E0, 0x00E3);
+ slickFont.addGlyphs(0x00E8, 0x00EA);
+ slickFont.addGlyphs(0x00EC, 0x00ED);
+ slickFont.addGlyphs(0x00F2, 0x00F5);
+ slickFont.addGlyphs(0x00F9, 0x00FA);
+ slickFont.addGlyphs(0x00FD, 0x00FD);
+ slickFont.addGlyphs(0x0102, 0x0103);
+ slickFont.addGlyphs(0x0110, 0x0111);
+ slickFont.addGlyphs(0x0128, 0x0129);
+ slickFont.addGlyphs(0x0168, 0x0169);
+ slickFont.addGlyphs(0x01A0, 0x01A1);
+ slickFont.addGlyphs(0x01AF, 0x01B0);
+ slickFont.addGlyphs(0x1EA0, 0x1EF9);
+
+ ((List) slickFont.getEffects()).add(new ColorEffect(Colour.WHITE.toAWT()));
slickFont.loadGlyphs();
}
- catch(FontFormatException | IOException | SlickException e) {
- e.printStackTrace();
+ catch(FontFormatException | IOException | SlickException error) {
+ // fallback to vanilla font
+ slickFont = null;
+ SolClientMod.instance.fancyFont = false;
+ LOGGER.error("Could not load font", error);
}
}
}
@@ -193,33 +219,28 @@ public void drawCenteredStringWithShadow(String text, float x, float y, int colo
}
public float getAscent() {
+ if(slickFont == null) return 0;
return slickFont.getAscent();
}
@Override
public float getWidth(String text) {
+ if(slickFont == null) return 0;
return slickFont.getWidth(EnumChatFormatting.getTextWithoutFormattingCodes(text)) / scaleFactor;
}
public float getCharWidth(char c) {
+ if(slickFont == null) return 0;
return slickFont.getWidth(String.valueOf(c));
}
public float getHeight(String s) {
+ if(slickFont == null) return 0;
return slickFont.getHeight(s) / 2.0F;
}
- public UnicodeFont getFont() {
- return slickFont;
- }
-
public void drawSplitString(ArrayList lines, int x, int y, int color) {
- renderString(
- String.join("\n\r", lines),
- x,
- y,
- color
- );
+ renderString(String.join("\n\r", lines), x, y, color);
}
public List splitString(String text, int wrapWidth) {
@@ -247,4 +268,9 @@ public List splitString(String text, int wrapWidth) {
public int getHeight() {
return 11;
}
+
+ public void free() {
+ slickFont.destroy();
+ }
+
}
diff --git a/game/src/main/resources/assets/sol_client/lang/en_GB.lang b/game/src/main/resources/assets/sol_client/lang/en_GB.lang
index 0e98a46c..c790e167 100644
--- a/game/src/main/resources/assets/sol_client/lang/en_GB.lang
+++ b/game/src/main/resources/assets/sol_client/lang/en_GB.lang
@@ -43,6 +43,8 @@ sol_client.mod.crosshair.option.entityColour=Entity Colour
sol_client.mod.scoreboard.description=Customise the scoreboard.
sol_client.mod.scoreboard.option.numbersColour=Numbers Colour
+sol_client.mod.tweaks.option.centredInventory=Centred Inventory
+
sol_client.mod.replay.option.recordingIndicatorColour=Recording Indicator Colour
sol_client.mod.replay.option.recordingIndicatorTextColour=Recording Indicator Text Colour
diff --git a/game/src/main/resources/assets/sol_client/lang/en_US.lang b/game/src/main/resources/assets/sol_client/lang/en_US.lang
index a769a7be..a4c81488 100644
--- a/game/src/main/resources/assets/sol_client/lang/en_US.lang
+++ b/game/src/main/resources/assets/sol_client/lang/en_US.lang
@@ -39,6 +39,7 @@ sol_client.mod.simple_hud.option.border=Border
sol_client.mod.simple_hud.option.borderColour=Border Color
sol_client.mod.simple_hud.option.textColour=Text Color
sol_client.mod.simple_hud.option.shadow=Text Shadow
+sol_client.mod.smooth_counter_hud.option.smoothNumbers=Smooth Numbers
sol_client.mod.sol_client.name=Sol Client
sol_client.mod.sol_client.description=General settings for Sol Client.
@@ -91,6 +92,11 @@ sol_client.mod.cps.option.separatorColour=Separator Color
sol_client.mod.ping.name=Ping
sol_client.mod.ping.description=Display the latency to the current server.
+sol_client.mod.ping.option.source=Source
+sol_client.mod.ping.option.source.auto=Auto
+sol_client.mod.ping.option.source.multiplayer_screen=Multiplayer Screen
+sol_client.mod.ping.option.source.tab_list=Tab List
+
sol_client.mod.speedometer.name=Speedometer
sol_client.mod.speedometer.description=Display your speed.
@@ -121,13 +127,15 @@ sol_client.mod.potion_effects.option.spacing=Spacing
sol_client.mod.armour.name=Armor and Equipment
sol_client.mod.armour.description=Display your armor and equipment with durability.
-sol_client.mod.armour.option.durability=Durability
+sol_client.mod.armour.option.durability=Durability Text
+sol_client.mod.armour.option.durability.off=Off
sol_client.mod.armour.option.durability.remaining=Remaining
sol_client.mod.armour.option.durability.percentage=Percentage
sol_client.mod.armour.option.durability.fraction=Fraction
sol_client.mod.armour.option.armour=Show Armor
sol_client.mod.armour.option.hand=Show Item
+sol_client.mod.armour.option.horizontal=Horizontal
sol_client.mod.timers.name=Timers
sol_client.mod.timers.description=Display timers for ingame events.
@@ -224,6 +232,7 @@ sol_client.mod.tweaks.option.confirmDisconnect=Confirm Disconnect
sol_client.mod.tweaks.option.borderlessFullscreen=Borderless Fullscreen
sol_client.mod.tweaks.option.betterKeyBindings=Better Key Bindings
sol_client.mod.tweaks.option.disableHotbarScrolling=Disable Hotbar Scrolling
+sol_client.mod.tweaks.option.centredInventory=Centered Inventory
sol_client.mod.tweaks.confirm_disconnect=Press Again to Confirm
diff --git a/game/src/main/resources/assets/sol_client/lang/es_MX.lang b/game/src/main/resources/assets/sol_client/lang/es_MX.lang
new file mode 100644
index 00000000..61138860
--- /dev/null
+++ b/game/src/main/resources/assets/sol_client/lang/es_MX.lang
@@ -0,0 +1,527 @@
+sol_client.passthrough=%s
+sol_client.slider.percent=%s%%
+sol_client.slider.factor=%sx
+sol_client.slider.seconds=%ss
+
+
+sol_client.vertical_alignment.top=Parte Más Alta
+sol_client.vertical_alignment.middle=Centro
+sol_client.vertical_alignment.bottom=Parte Más Abaja
+
+
+sol_client.perspective.first_person=Primera Person
+sol_client.perspective.third_person_back=Tercera Persona Detras
+sol_client.perspective.third_person_front=Tercera Person Frente
+
+
+sol_client.easing.linear=Lineal
+sol_client.easing.quad=Cuadrático
+sol_client.easing.cubic=Cúbico
+sol_client.easing.quart=Cúartico
+sol_client.easing.quint=Qúintico
+sol_client.easing.expo=Exponencial
+sol_client.easing.sine=Seno
+sol_client.easing.circ=Circular
+sol_client.easing.back=Reverso
+sol_client.easing.bounce=Rebote
+sol_client.easing.elastic=Elástico
+
+
+sol_client.file.edit=Editar Archivo...
+
+
+sol_client.servers=Servidores
+
+
+sol_client.key.category=Sol Client
+sol_client.key.accept_request=Aceptar Petición
+sol_client.key.dismiss_request=Descartar Petición
+
+
+sol_client.mod.generic.option.enabled=Activado
+sol_client.mod.hud.option.scale=Escala
+sol_client.mod.simple_hud.option.background=Fondo
+sol_client.mod.simple_hud.option.backgroundColour=Color Del Fondo
+sol_client.mod.simple_hud.option.border=Borde
+sol_client.mod.simple_hud.option.borderColour=Color Del Borde
+sol_client.mod.simple_hud.option.textColour=Color Del Texto
+sol_client.mod.simple_hud.option.shadow=Sombra Del Texto
+
+
+sol_client.mod.sol_client.name=Sol Client
+sol_client.mod.sol_client.description=Escenarios generales para Sol Client.
+
+
+
+
+sol_client.mod.sol_client.option.fancyMainMenu=Menú Principal
+sol_client.mod.sol_client.option.logoInInventory=Enseñar Logotipo En El Inventario
+sol_client.mod.sol_client.mods=Modificaciónes
+sol_client.mod.sol_client.option.modsKey=Tecla De Modificaciónes
+sol_client.mod.sol_client.edit_hud=Editar Monitor
+sol_client.mod.sol_client.option.editHudKey=Tecla De Editar Monitor
+sol_client.mod.sol_client.option.uiColour=Color Acento
+sol_client.mod.sol_client.option.smoothUIColours=Suavizar Colores De Interfaz De Usuario
+sol_client.mod.sol_client.option.roundedUI=Interfaz De Usuario Redondeado
+sol_client.mod.sol_client.option.buttonClicks=Clices De Botón
+sol_client.mod.sol_client.option.smoothScrolling=Desplazamiento suave
+sol_client.mod.sol_client.option.fancyFont=Tipografía de alta definición
+
+
+sol_client.mod.fps.name=FPS
+sol_client.mod.fps.description=Mostrar las FPS (fotogramas por segundo).
+
+
+sol_client.mod.coordinates.name=Coordenadas
+sol_client.mod.coordinates.description=Mostrar tus coordenadas.
+
+
+sol_client.mod.coordinates.option.axisLabelColour=Color De Eje Etiqueta
+sol_client.mod.coordinates.option.axisValueColour=Color De Eje Valor
+sol_client.mod.coordinates.option.cardinalDirection=Dirección Cardinal
+sol_client.mod.coordinates.option.cardinalDirectionColour=Color De Dirreción Cardinal
+sol_client.mod.coordinates.option.axisDirection=Dirreción De Eje
+sol_client.mod.coordinates.option.axisDirectionColour=Color De Dirreción De Eje
+
+
+sol_client.mod.keystrokes.name=Pulsaciónes
+sol_client.mod.keystrokes.description=Mostrar teclas presionadas.
+
+
+sol_client.mod.keystrokes.option.movement=Movimiento
+sol_client.mod.keystrokes.option.mouse=Ratón
+sol_client.mod.keystrokes.option.mouseMovement=Movimiento De Ratón
+sol_client.mod.keystrokes.option.showSpace=Espacio
+sol_client.mod.keystrokes.option.cps=CPS en Teclas
+sol_client.mod.keystrokes.option.backgroundColourPressed=Color De Fondo(Presiondao)
+sol_client.mod.keystrokes.option.borderColourPressed=Color De Borde(Presionado)
+sol_client.mod.keystrokes.option.textColourPressed=Color De Texto(Presionado)
+sol_client.mod.keystrokes.option.smoothColours=Colores Suaves
+
+
+sol_client.mod.cps.name=CPS
+sol_client.mod.cps.description=Mostrar tus CPS (clices por segundo).
+
+
+sol_client.mod.cps.option.rmb=CPS de botón derecho del ratón
+sol_client.mod.cps.option.separatorColour=Color de separador
+
+
+sol_client.mod.ping.name=Ping
+sol_client.mod.ping.description=Mostrar la latencia al servidor actual.
+
+
+sol_client.mod.speedometer.name=Velocímetro
+sol_client.mod.speedometer.description=Mostar tu velocidad.
+
+
+sol_client.mod.speedometer.option.graphMode=Ver con gráficas
+
+
+sol_client.mod.reach_display.name=Mostar distancia de alcance.
+sol_client.mod.reach_display.description=Mostar la distancia cuando pegas entidades
+
+
+sol_client.mod.combo_counter.name=Combo Contador
+sol_client.mod.combo_counter.description=Contar el número de golpes posteriores
+
+
+sol_client.mod.combo_counter.no_hits=No combo
+sol_client.mod.combo_counter.one_hit=1 golpe
+sol_client.mod.combo_counter.n_hits=%s golpes
+
+
+sol_client.mod.potion_effects.name=Efectos de pociones
+sol_client.mod.potion_effects.description=Mostar tus pociones activos con temporizadores de cuenta atrás.
+
+
+sol_client.mod.potion_effects.option.alignment=Alineación
+sol_client.mod.potion_effects.option.icon=Icono
+sol_client.mod.potion_effects.option.background=Fondo
+sol_client.mod.potion_effects.option.title=Etiqueta
+sol_client.mod.potion_effects.option.titleColour=Color de etiqueta
+sol_client.mod.potion_effects.option.duration=Duración
+sol_client.mod.potion_effects.option.durationColour=Color de duración
+sol_client.mod.potion_effects.option.spacing=Espacio
+
+
+sol_client.mod.armour.name=Armadura y equipamiento
+sol_client.mod.armour.description=Mostar tu armadura y equipamiento con durabilidad.
+
+
+sol_client.mod.armour.option.durability=Durabilidad
+sol_client.mod.armour.option.durability.remaining=Restante
+sol_client.mod.armour.option.durability.percentage=Porcentaje
+sol_client.mod.armour.option.durability.fraction=Fracción
+
+
+sol_client.mod.armour.option.armour=Mostar armadura
+sol_client.mod.armour.option.hand=Mostar artículo
+sol_client.mod.timers.name=Temporizadores
+sol_client.mod.timers.description=Mostar temporizadores para eventos del juego.
+
+
+sol_client.mod.timers.option.alignment=Alineación
+sol_client.mod.timers.option.icon=Icono
+sol_client.mod.timers.option.nameColour=Color de etiqueta
+sol_client.mod.timers.option.timeColour=Color de tiempo
+
+
+sol_client.mod.chat.name=Chateo
+sol_client.mod.chat.description=Customizar chateo del juego.
+
+
+sol_client.mod.chat.peek=Vistazo
+sol_client.mod.chat.option.preventClose=Prevenir cerrar
+sol_client.mod.chat.option.smooth=Animación de mensaje
+sol_client.mod.chat.option.infiniteChat=Chateo infinito
+sol_client.mod.chat.option.peekKey=Tecla para echar un vistazo
+
+
+sol_client.mod.chat.option.visibility=Visibilidad
+sol_client.mod.chat.option.visibility.shown=Mostrado
+sol_client.mod.chat.option.visibility.commands=Solo comandos
+sol_client.mod.chat.option.visibility.hidden=Escondido
+
+
+sol_client.mod.chat.option.defaultTextColour=Predeterminado color de texto
+sol_client.mod.chat.option.colours=Colores
+sol_client.mod.chat.option.width=Ancho
+sol_client.mod.chat.option.closedHeight=Altura cerrada
+sol_client.mod.chat.option.openHeight=Altura abierta
+sol_client.mod.chat.option.links=Enlaces
+sol_client.mod.chat.option.promptLinks=Prompt Enlaces
+sol_client.mod.chat.option.chatFilter=Activar filtro de chateo
+sol_client.mod.chat.option.filteredWordsContent=Palabras Filtradas
+
+
+sol_client.mod.tab_list.name=Lista de Tabulador.
+sol_client.mod.tab_list.description=Customizar lista de tabulador.
+
+
+sol_client.mod.tab_list.option.hideHeader=Esconder encabezado.
+sol_client.mod.tab_list.option.hideFooter=Esconder pie de página
+
+
+sol_client.mod.tab_list.option.pingType=Ping
+sol_client.mod.tab_list.option.pingType.none=Escondido
+sol_client.mod.tab_list.option.pingType.icon=Icono
+sol_client.mod.tab_list.option.pingType.numeral=Número
+
+
+sol_client.mod.tab_list.option.backgroundColour=Color de fondo de lista
+sol_client.mod.tab_list.option.entryBackgroundColour=Color de fondo de entrada
+sol_client.mod.tab_list.option.hidePlayerHeads=Esconder cabezas de jugadores
+sol_client.mod.tab_list.option.textShadow=Sombra de texto
+
+
+sol_client.mod.crosshair.name=Punto de mira
+sol_client.mod.crosshair.description=Personalizar el punto de mira.
+
+
+sol_client.mod.crosshair.option.style=Estilo
+sol_client.mod.crosshair.option.style.default=Defecto
+sol_client.mod.crosshair.option.style.none=Nada
+sol_client.mod.crosshair.option.style.dot=Punto
+sol_client.mod.crosshair.option.style.plus=Plus
+sol_client.mod.crosshair.option.style.plus_dot=Punto plus
+sol_client.mod.crosshair.option.style.square=Cuadrado
+sol_client.mod.crosshair.option.style.square_dot=Punto cuadrado
+sol_client.mod.crosshair.option.style.circle=Circulo
+sol_client.mod.crosshair.option.style.circle_dot=Punto circular
+sol_client.mod.crosshair.option.style.four_angled=X
+sol_client.mod.crosshair.option.style.four_angled_dot=X con punto
+sol_client.mod.crosshair.option.style.triangle=Triángulo
+
+
+sol_client.mod.crosshair.option.thirdPerson=Mostrar en tercera persona
+sol_client.mod.crosshair.option.spectatorAlways=Mostrar en modo espectador
+sol_client.mod.crosshair.option.debug=Mostrar en superposición de depuración
+sol_client.mod.crosshair.option.blending=Mezclar
+sol_client.mod.crosshair.option.crosshairColour=Color
+sol_client.mod.crosshair.option.highlightEntities=Resaltar entidades
+sol_client.mod.crosshair.option.entityColour=Color de entidades
+
+
+sol_client.mod.scoreboard.name=Marcador
+sol_client.mod.scoreboard.description=Personalizar el marcador.
+
+
+sol_client.mod.scoreboard.option.backgroundColourTop=Color de fondo superior
+sol_client.mod.scoreboard.option.numbers=Números
+sol_client.mod.scoreboard.option.numbersColour=Colores de números
+
+
+sol_client.mod.tweaks.name=Modificaciones
+sol_client.mod.tweaks.description=Varias modificaciones incluyendo fullbright.
+
+
+sol_client.mod.tweaks.option.fullbright=Fullbright
+sol_client.mod.tweaks.option.showOwnTag=Mostrar tu propio Nametag
+sol_client.mod.tweaks.option.arabicNumerals=Números arábigos
+sol_client.mod.tweaks.option.betterTooltips=Mejorar Tooltips
+sol_client.mod.tweaks.option.minimalViewBobbing=Balanceo mínimo
+sol_client.mod.tweaks.option.minimalDamageShake=Agitar de daño mínimo
+sol_client.mod.tweaks.option.damageShakeIntensity=Intensidad de agitar de daño
+sol_client.mod.tweaks.option.confirmDisconnect=Confirmar desconexión
+sol_client.mod.tweaks.option.borderlessFullscreen=Pantalla completa sin bordes
+sol_client.mod.tweaks.option.betterKeyBindings=Mejorado enlaces de teclas
+sol_client.mod.tweaks.option.disableHotbarScrolling=Desactivar desplazarte barra de acceso directo.
+
+
+sol_client.mod.tweaks.confirm_disconnect=Pulsa de nuevo para confirmar.
+
+
+sol_client.mod.freelook.name=Mirada libre
+sol_client.mod.freelook.description=Cambiar tu perspectiva sin girar tu jugador.
+
+
+sol_client.mod.freelook.key=Mirada libre
+sol_client.mod.freelook.option.key=Tecla
+
+
+sol_client.mod.freelook.option.perspective=Perspectiva
+sol_client.mod.freelook.option.invertPitch=Invertir verticalmente
+sol_client.mod.freelook.option.invertYaw=Invertir horizontalmente
+
+
+sol_client.mod.taplook.name=Mirada de golpear suavemente
+sol_client.mod.taplook.description=Modificar su perspectiva pulsando un tecla
+
+
+sol_client.mod.taplook.key=Mirada de golpear suavemente
+sol_client.mod.taplook.option.key=Tecla
+sol_client.mod.taplook.option.perspective=Perspectiva
+
+
+sol_client.mod.toggle_sprint.name=Alternar sprint
+sol_client.mod.toggle_sprint.description=Hace que la tecla de sprint puede alternar el sprint.
+
+
+sol_client.mod.toggle_sprint.default_width=113
+
+
+sol_client.mod.toggle_sprint.option.hud=HUD
+
+
+sol_client.mod.toggle_sprint.held=Sprinting (Pulsado)
+sol_client.mod.toggle_sprint.toggled=Sprinting (Alternado)
+
+
+sol_client.mod.zoom.name=Zoom
+sol_client.mod.zoom.description=Acercar al pulsar un botón
+
+
+sol_client.mod.zoom.key=Zoom
+sol_client.mod.zoom.option.key=Tecla
+sol_client.mod.zoom.zoom_out=Alejar
+sol_client.mod.zoom.option.zoomOutKey=Tecla de alejar
+sol_client.mod.zoom.zoom_in=Acercar
+sol_client.mod.zoom.option.zoomInKey=Tecla de acercar
+sol_client.mod.zoom.option.cinematic=Cámara cinematográfica
+sol_client.mod.zoom.option.reduceSensitivity=Reducir la sensibilidad
+sol_client.mod.zoom.option.scrolling=Desplácete para zoom
+sol_client.mod.zoom.option.smooth=Animación de zoom
+sol_client.mod.zoom.option.factor=Factor de zoom
+
+
+sol_client.mod.replay.name=Reproducción
+sol_client.mod.replay.description=Grabar y reproducir tu juego
+
+
+sol_client.mod.replay.option.enableNotifications=Activar las notificaciones
+sol_client.mod.replay.option.recordSingleplayer=Grabar un solo jugador
+sol_client.mod.replay.option.recordServer=Grabar servidor
+sol_client.mod.replay.option.recordingIndicator=Indicador de grabación
+sol_client.mod.replay.option.recordingIndicatorColour=Color de indicador de grabación
+sol_client.mod.replay.option.recordingIndicatorScale=Tamaño de indicador de grabación
+sol_client.mod.replay.option.recordingIndicatorTextColour=Color de texto para indicador de grabación
+sol_client.mod.replay.option.recordingIndicatorTextShadow=Sombra de texto para indicador de grabación
+sol_client.mod.replay.option.automaticRecording=Grabación automática
+sol_client.mod.replay.option.renameDialog=Renombrar diálogo
+sol_client.mod.replay.option.showChat=Mostrar chat
+
+
+sol_client.mod.replay.option.camera=Cámara
+sol_client.mod.replay.option.camera.classic=Clásico
+sol_client.mod.replay.option.camera.vanilla_ish=Sin modificaciones
+
+
+sol_client.mod.replay.option.showPathPreview=Mostrar la trayectoria de la cámara
+
+
+sol_client.mod.replay.option.defaultInterpolator=Interpolador por defecto
+sol_client.mod.replay.interpolator.catmull=Spline de Catmull-Rom
+sol_client.mod.replay.interpolator.cubic=Spline cúbico
+sol_client.mod.replay.interpolator.linear=Lineal
+
+
+sol_client.mod.replay.option.showServerIPs=Mostrar los IPs de servidores.
+
+
+sol_client.mod.quickplay.name=Partida rápida
+sol_client.mod.quickplay.description=Empezar un partido rápidamente usando su teclado.
+
+
+sol_client.mod.quickplay.key=Partida rápida
+sol_client.mod.quickplay.option.menuKey=Tecla
+
+
+sol_client.mod.hypixel_util.name=Ajustes de Hypixel
+sol_client.mod.hypixel_util.description=Mejoras varias para la experiencia en Hypixel.
+
+
+sol_client.mod.hypixel_util.option.visitHousingCommand=/visithousing
+sol_client.mod.hypixel_util.option.lobbySoundsVolume=Volumen de sonidas en lobby
+sol_client.mod.hypixel_util.option.housingMusicVolume=Volumen de música en Housing
+sol_client.mod.hypixel_util.option.autogg=Auto GG
+sol_client.mod.hypixel_util.option.autoggMessage=Mensaje de GG
+sol_client.mod.hypixel_util.option.hidegg=Esconder GG
+sol_client.mod.hypixel_util.option.autogl=Auto GL
+sol_client.mod.hypixel_util.option.autoglMessage=Mensaje de GL
+sol_client.mod.hypixel_util.option.hidegl=Esconder GL
+sol_client.mod.hypixel_util.option.levelhead=Nivel en la cabeza
+
+
+sol_client.mod.tnt_timer.name=Cuenta regresiva para TNT
+sol_client.mod.tnt_timer.description=Mostrar una cuenta regresiva para TNT.
+
+
+sol_client.mod.motion_blur.name=Desenfoque de movimiento
+sol_client.mod.motion_blur.description=Efecto de desenfoque de movimiento.
+
+
+sol_client.mod.motion_blur.option.blur=Desenfocar
+
+
+sol_client.mod.menu_blur.name=Desenfocar menús
+sol_client.mod.menu_blur.description=Desenfoque el fondo de menús.
+
+
+sol_client.mod.menu_blur.option.blur=Difuminar
+sol_client.mod.menu_blur.option.fadeTime=Tiempo para disiparse
+sol_client.mod.menu_blur.option.backgroundColour=Color de fondo
+
+
+sol_client.mod.colour_saturation.name=Saturación de color
+sol_client.mod.colour_saturation.description=Adjustar la saturación de color del mundo.
+
+
+sol_client.mod.colour_saturation.option.saturation=Saturación
+
+
+sol_client.mod.chunk_animator.name=Animador de chunks
+sol_client.mod.chunk_animator.description=Hace un animación del mundo mientras carga.
+
+
+sol_client.mod.chunk_animator.option.duration=Duración
+sol_client.mod.chunk_animator.option.animation=Animacion
+
+
+sol_client.mod.1.7_visuals.name=Visuales de 1.7
+sol_client.mod.1.7_visuals.description=Revertir algunas animaciones de 1.7.
+
+
+sol_client.mod.1.7_visuals.option.useAndMine=Usar item mientras excavas
+sol_client.mod.1.7_visuals.option.particles=Partículas
+sol_client.mod.1.7_visuals.option.blocking=Bloquear con espada
+sol_client.mod.1.7_visuals.option.eatingAndDrinking=Comer y beber
+sol_client.mod.1.7_visuals.option.rod=Caña de pescar
+sol_client.mod.1.7_visuals.option.bow=Arco
+sol_client.mod.1.7_visuals.option.armourDamage=Color de daño en armadura
+sol_client.mod.1.7_visuals.option.sneaking=Agachar suave
+
+
+sol_client.mod.item_physics.name=Item Física de los Items
+sol_client.mod.item_physics.description=Añadir una animación giratoria a los ítems.
+
+
+sol_client.mod.item_physics.option.rotationSpeed=Velocidad de rotación
+
+
+sol_client.mod.particles.name=Partículas
+sol_client.mod.particles.description=Modificar partículas de ataque.
+
+
+sol_client.mod.particles.option.multiplier=Multiplicador
+sol_client.mod.particles.option.sharpness=Siempre mostrar afilado
+sol_client.mod.particles.option.snow=Nieve
+sol_client.mod.particles.option.slime=Slime
+sol_client.mod.particles.option.flames=Llamas
+
+
+sol_client.mod.time_changer.name=Cambiador de Tiempo
+sol_client.mod.time_changer.description=Ajustar la hora visual sin modificar la hora real.
+
+
+sol_client.mod.time_changer.option.time=Tiempo
+
+
+sol_client.mod.block_selection.name=Selección de Bloques
+sol_client.mod.block_selection.description=Modificar el contorno y color del bloque al que estás apuntado.
+
+
+sol_client.mod.block_selection.option.outline=Contorno
+sol_client.mod.block_selection.option.outlineWidth=Ancho de contorno
+sol_client.mod.block_selection.option.outlineColour=Color de contorno
+sol_client.mod.block_selection.option.fill=Aplicar un color
+sol_client.mod.block_selection.option.fillColour=Color de llenar
+sol_client.mod.block_selection.option.depth=Profundidad
+sol_client.mod.block_selection.option.persistent=Persistente
+
+
+sol_client.mod.hit_colour.name=Color de golpe
+sol_client.mod.hit_colour.description=Cambiar el color de golpe.
+
+
+sol_client.mod.hit_colour.option.colour=Color
+
+
+sol_client.mod.hitbox.name=Hitbox
+sol_client.mod.hitbox.description=Modificar y conmutar hitboxes fácilmente.
+
+
+sol_client.mod.hitbox.option.toggleHitboxes=Conmutar hitbox
+sol_client.mod.hitbox.option.boundingBox=Color de hitbox
+sol_client.mod.hitbox.option.boundingBoxColour=Color de hitbox
+sol_client.mod.hitbox.option.eyeHeight=Monstrar punto de vista
+sol_client.mod.hitbox.option.eyeHeightColour=Color de punto de vista
+sol_client.mod.hitbox.option.lookVector=Monstrar vector de mirada
+sol_client.mod.hitbox.option.lookVectorColour=Color de vector de mirada
+sol_client.mod.hitbox.option.lineWidth=Ancho de línea
+
+
+sol_client.mod.category.general.name=General
+sol_client.mod.category.hud.name=HUD
+sol_client.mod.category.utility.name=Utilidad
+sol_client.mod.category.visual.name=Visual
+sol_client.mod.category.integration.name=Integración
+
+
+sol_client.mod.discord_integration.name=Integración de discord
+sol_client.mod.discord_integration.description=Funciones de integración para Discord (actividades y posición).
+
+
+sol_client.mod.discord_integration.option.voiceChatHud=HUD del chat de voz
+sol_client.mod.discord_integration.option.voiceChatHudScale=Tamaño del HUD del chat de voz
+sol_client.mod.discord_integration.option.voiceChatHudAlignment=Posición del HUD del chat de voz
+sol_client.mod.discord_integration.option.usernameColour=Color de nombre
+sol_client.mod.discord_integration.option.mutedColour=Color de personas silenciadas
+sol_client.mod.discord_integration.option.speakingColour=Color cuando hablando
+
+
+sol_client.mod.screen.title=Mods
+sol_client.mod.screen.search=Escribe para buscar
+sol_client.mod.screen.apply_to_all=Aplicar a todos
+
+
+sol_client.hud.edit=Modificar HUD
+
+
+sol_client.packs.apply=Applicar
+sol_client.packs.open_folder=Carpeta de paquetes
+sol_client.packs.folder=Carpeta (%d items)
+
+
+sol_client.screenshot.view=Ver
+sol_client.screenshot.open_folder=Monstrar en carpeta
diff --git a/game/src/main/resources/assets/sol_client/lang/fi_FI.lang b/game/src/main/resources/assets/sol_client/lang/fi_FI.lang
new file mode 100644
index 00000000..815cfbc2
--- /dev/null
+++ b/game/src/main/resources/assets/sol_client/lang/fi_FI.lang
@@ -0,0 +1,442 @@
+sol_client.passthrough=%s
+sol_client.slider.percent=%s%%
+sol_client.slider.factor=%sx
+sol_client.slider.seconds=%ss
+
+sol_client.vertical_alignment.top=Yläosa
+sol_client.vertical_alignment.middle=Keskiosa
+sol_client.vertical_alignment.bottom=Alaosa
+
+sol_client.perspective.first_person=Ensimmäinen Persoona
+sol_client.perspective.third_person_back=Kolmas Persoona Takaa
+sol_client.perspective.third_person_front=Kolmas Persoona Edestä
+
+sol_client.easing.linear=Lineaarinen
+sol_client.easing.quad=Quad
+sol_client.easing.cubic=Cubic
+sol_client.easing.quart=Quart
+sol_client.easing.quint=Quint
+sol_client.easing.expo=Expo
+sol_client.easing.sine=Sine
+sol_client.easing.circ=Circ
+sol_client.easing.back=Back
+sol_client.easing.bounce=Bounce
+sol_client.easing.elastic=Elastinen
+
+sol_client.file.edit=Muokkaa tiedostoa...
+
+sol_client.servers=Palvelimet
+
+sol_client.key.category=Sol Client
+sol_client.key.accept_request=Hyväksy Pyyntö
+sol_client.key.dismiss_request=Hylkää Pyyntö
+
+sol_client.mod.generic.option.enabled=Käytössä
+sol_client.mod.hud.option.scale=Mittakaava
+sol_client.mod.simple_hud.option.background=Tausta
+sol_client.mod.simple_hud.option.backgroundColour=Taustan Väri
+sol_client.mod.simple_hud.option.border=Raja
+sol_client.mod.simple_hud.option.borderColour=Rajan Väri
+sol_client.mod.simple_hud.option.textColour=Tekstin Väri
+sol_client.mod.simple_hud.option.shadow=Tekstin Varjo
+sol_client.mod.smooth_counter_hud.option.smoothNumbers=Pehmeät Numerot
+
+sol_client.mod.sol_client.name=Sol Client
+sol_client.mod.sol_client.description=Yleiset asetukset.
+
+sol_client.mod.sol_client.option.fancyMainMenu=Päävalikko
+sol_client.mod.sol_client.option.logoInInventory=Näytä Logo Tavaraluettelossa
+sol_client.mod.sol_client.mods=Mods
+sol_client.mod.sol_client.option.modsKey=Mods valikon näppäin
+sol_client.mod.sol_client.edit_hud=Mukauta HUD
+sol_client.mod.sol_client.option.editHudKey=Vaihda HUD näppäin
+sol_client.mod.sol_client.option.uiColour=Aksentin Väri
+sol_client.mod.sol_client.option.smoothUIColours=Pehmeät UI Värit
+sol_client.mod.sol_client.option.roundedUI=Pyöreä UI
+sol_client.mod.sol_client.option.buttonClicks=Painikkeen napsautukset
+sol_client.mod.sol_client.option.smoothScrolling=Pehmeä Vieritys
+sol_client.mod.sol_client.option.fancyFont=HD Fontti
+
+sol_client.mod.fps.name=FPS
+sol_client.mod.fps.description=Näytä FPS (kuvat sekunnissa).
+
+sol_client.mod.coordinates.name=Kordinaatit
+sol_client.mod.coordinates.description=Näytä kordinaattisi.
+
+sol_client.mod.coordinates.option.axisLabelColour=Akselin etiketin väri
+sol_client.mod.coordinates.option.axisValueColour=Akselin arvon väri
+sol_client.mod.coordinates.option.cardinalDirection=Kardinaali suunta
+sol_client.mod.coordinates.option.cardinalDirectionColour=Kardinaalin suunnan Väri
+sol_client.mod.coordinates.option.axisDirection=Akselin Suunta
+sol_client.mod.coordinates.option.axisDirectionColour=Akselin suunnan väri
+
+sol_client.mod.keystrokes.name=Näppäinpainallukset
+sol_client.mod.keystrokes.description=Näytä pohjassa painetut näppäimet.
+
+sol_client.mod.keystrokes.option.movement=Liikkuminen
+sol_client.mod.keystrokes.option.mouse=Hiiri
+sol_client.mod.keystrokes.option.mouseMovement=Hiiren Liike
+sol_client.mod.keystrokes.option.showSpace=Välilyönti
+sol_client.mod.keystrokes.option.cps=CPS näppäimissä
+sol_client.mod.keystrokes.option.backgroundColourPressed=Taustan Väri (Painettu)
+sol_client.mod.keystrokes.option.borderColourPressed=Rajan Väri (Painettu)
+sol_client.mod.keystrokes.option.textColourPressed=Tekstin Väri (Painettu)
+sol_client.mod.keystrokes.option.smoothColours=Pehmeät Värit
+
+sol_client.mod.cps.name=CPS
+sol_client.mod.cps.description=Näytä CPS (painallukset sekunnissa).
+
+sol_client.mod.cps.option.rmb=Hiiren oikean näppäimen CPS
+sol_client.mod.cps.option.separatorColour=Erottimen Väri
+
+sol_client.mod.ping.name=Viive
+sol_client.mod.ping.description=Näytä viive nykyiselle palvelimelle.
+
+sol_client.mod.ping.option.source=Lähde
+sol_client.mod.ping.option.source.auto=Automaattinen
+sol_client.mod.ping.option.source.multiplayer_screen=Moninpeli Näyttö
+sol_client.mod.ping.option.source.tab_list=Tab List
+
+sol_client.mod.speedometer.name=Nopeusmittari
+sol_client.mod.speedometer.description=Näytä nopeutesi.
+
+sol_client.mod.speedometer.option.graphMode=Kuvaaja
+
+sol_client.mod.reach_display.name=Ulotusmittari
+sol_client.mod.reach_display.description=Näytä etäisyys osuessaan entiteettiin.
+
+sol_client.mod.combo_counter.name=Combon Laskija
+sol_client.mod.combo_counter.description=Laske peräkkäisien osumien määrä.
+
+sol_client.mod.combo_counter.no_hits=Ei comboa
+sol_client.mod.combo_counter.one_hit=1 osuma
+sol_client.mod.combo_counter.n_hits=%s osumaa
+
+sol_client.mod.potion_effects.name=Näytä Taikajuomat
+sol_client.mod.potion_effects.description=Näytä aktiiviset taikajuomat laskimella.
+
+sol_client.mod.potion_effects.option.alignment=Linjaus
+sol_client.mod.potion_effects.option.icon=Kuva
+sol_client.mod.potion_effects.option.background=Tausta
+sol_client.mod.potion_effects.option.title=Etiketti
+sol_client.mod.potion_effects.option.titleColour=Etiketin Väri
+sol_client.mod.potion_effects.option.duration=Aika
+sol_client.mod.potion_effects.option.durationColour=Ajan Väri
+sol_client.mod.potion_effects.option.spacing=Välitys
+
+sol_client.mod.armour.name=Panssari ja Varusteet
+sol_client.mod.armour.description=Näytä panssarisi ja varusteet käyttömäärän kanssa.
+
+sol_client.mod.armour.option.durability=Käyttömäärän Teksti
+sol_client.mod.armour.option.durability.off=Pois
+sol_client.mod.armour.option.durability.remaining=Jäljellä
+sol_client.mod.armour.option.durability.percentage=Prosentti
+sol_client.mod.armour.option.durability.fraction=Murto-osa
+
+sol_client.mod.armour.option.armour=Näytä panssari
+sol_client.mod.armour.option.hand=Näytä Käsi
+sol_client.mod.armour.option.horizontal=Vaakasuora
+
+sol_client.mod.timers.name=Ajastimet
+sol_client.mod.timers.description=Näytä ajastimet pelin-sisäisiin tapahtumiin.
+
+sol_client.mod.timers.option.alignment=Linjaus
+sol_client.mod.timers.option.icon=Kuvake
+sol_client.mod.timers.option.nameColour=Etiketin väri
+sol_client.mod.timers.option.timeColour=Ajan väri
+
+sol_client.mod.chat.name=Chat
+sol_client.mod.chat.description=Mukauta pelinsisäistä chat keskustelua.
+
+sol_client.mod.chat.peek=Chat keskustelun kurkistaminen
+sol_client.mod.chat.option.preventClose=Estä Sulkeminen
+sol_client.mod.chat.option.smooth=Viestin Animaatio
+sol_client.mod.chat.option.infiniteChat=Ääretön Chat Keskustelu
+sol_client.mod.chat.option.peekKey=Kurkistamisen näppäin
+
+sol_client.mod.chat.option.visibility=Näkyvyys
+sol_client.mod.chat.option.visibility.shown=Näytetään
+sol_client.mod.chat.option.visibility.commands=Vain Komennot
+sol_client.mod.chat.option.visibility.hidden=Piilotettu
+
+sol_client.mod.chat.option.defaultTextColour=Tekstin Oletusväri
+sol_client.mod.chat.option.colours=Värit
+sol_client.mod.chat.option.width=Leveys
+sol_client.mod.chat.option.closedHeight=Suljettu korkeus
+sol_client.mod.chat.option.openHeight=Avoin korkeus
+sol_client.mod.chat.option.links=Linkit
+sol_client.mod.chat.option.promptLinks=Pikalinkit
+sol_client.mod.chat.option.chatFilter=Ota chat-suodatin käyttöön
+sol_client.mod.chat.option.filteredWordsContent=Suodatetut Sanat
+
+sol_client.mod.tab_list.name=Tab Lista
+sol_client.mod.tab_list.description=Mukauta tab- tai pelaajaluoteeloa.
+
+sol_client.mod.tab_list.option.hideHeader=Piilota Otsikko
+sol_client.mod.tab_list.option.hideFooter=Piilota Alatunniste
+
+sol_client.mod.tab_list.option.pingType=Viive
+sol_client.mod.tab_list.option.pingType.none=Piilotettu
+sol_client.mod.tab_list.option.pingType.icon=Kuvake
+sol_client.mod.tab_list.option.pingType.numeral=Numero
+
+sol_client.mod.tab_list.option.backgroundColour=Luettelon taustaväri
+sol_client.mod.tab_list.option.entryBackgroundColour=Sisääntulon taustaväri
+sol_client.mod.tab_list.option.hidePlayerHeads=Piilota pelaajien päät
+sol_client.mod.tab_list.option.textShadow=Tekstin Varjo
+
+sol_client.mod.crosshair.name=Tähtäin
+sol_client.mod.crosshair.description=Mukauta tähtäintä.
+
+sol_client.mod.crosshair.option.style=Tyyli
+sol_client.mod.crosshair.option.style.default=Oletus
+sol_client.mod.crosshair.option.style.none=Ei mikään
+sol_client.mod.crosshair.option.style.dot=Piste
+sol_client.mod.crosshair.option.style.plus=Plus
+sol_client.mod.crosshair.option.style.plus_dot=Plus Piste
+sol_client.mod.crosshair.option.style.square=Neliö
+sol_client.mod.crosshair.option.style.square_dot=Neliö Piste
+sol_client.mod.crosshair.option.style.circle=Ympyrä
+sol_client.mod.crosshair.option.style.circle_dot=Ympyrä Piste
+sol_client.mod.crosshair.option.style.four_angled=4 Kulma
+sol_client.mod.crosshair.option.style.four_angled_dot=4 Kulmainen Piste
+sol_client.mod.crosshair.option.style.triangle=Kolmio
+
+sol_client.mod.crosshair.option.thirdPerson=Näytä Kolmannessa Persoonassa
+sol_client.mod.crosshair.option.spectatorAlways=Näytä katsojatilassa
+sol_client.mod.crosshair.option.debug=Näytä virheenkorjauspeittokuvassa
+sol_client.mod.crosshair.option.blending=Sekoitus
+sol_client.mod.crosshair.option.crosshairColour=Väri
+sol_client.mod.crosshair.option.highlightEntities=Korosta Entiteetit
+sol_client.mod.crosshair.option.entityColour=Entiteettien Väri
+
+sol_client.mod.scoreboard.name=Scoreboard
+sol_client.mod.scoreboard.description=Mukauta scoreboard:ia.
+
+sol_client.mod.scoreboard.option.hide=Piilota
+sol_client.mod.scoreboard.option.backgroundColourTop=Yläosan taustaväri
+sol_client.mod.scoreboard.option.numbers=Numerot
+sol_client.mod.scoreboard.option.numbersColour=Numeroiden Väri
+
+sol_client.mod.tweaks.name=Säätöjä
+sol_client.mod.tweaks.description=Erilaisia pelisäätöjä, mukaan lukien fullbright.
+
+sol_client.mod.tweaks.option.fullbright=Täysi Kirkkaus
+sol_client.mod.tweaks.option.showOwnTag=Näytä oma nimilappu
+sol_client.mod.tweaks.option.arabicNumerals=Arabialaiset numerot
+sol_client.mod.tweaks.option.betterTooltips=Paremmat Työkäluvinkit
+sol_client.mod.tweaks.option.minimalViewBobbing=Vähäinen Kuvan Keikkuminen
+sol_client.mod.tweaks.option.minimalDamageShake=Vähäinen vaurion tärinä
+sol_client.mod.tweaks.option.damageShakeIntensity=Vaurion tärinän voimakkuus
+sol_client.mod.tweaks.option.confirmDisconnect=Vahvista Yhteyden Katkaiseminen
+sol_client.mod.tweaks.option.borderlessFullscreen=Reunaton Kokonäyttö
+sol_client.mod.tweaks.option.betterKeyBindings=Paremmat näppäinasetukset
+sol_client.mod.tweaks.option.disableHotbarScrolling=Poista Hotbar-vieritys käytöstä
+sol_client.mod.tweaks.option.centredInventory=Keskitetty tavaraluettelo
+
+sol_client.mod.tweaks.confirm_disconnect=Paina uudestaan vahvistaaksesi
+
+sol_client.mod.freelook.name=Vapaakatse
+sol_client.mod.freelook.description=Irrota kameran pyöriminen pelaajan päästä.
+
+sol_client.mod.freelook.key=Vapaakatse
+sol_client.mod.freelook.option.key=Näppäin
+
+sol_client.mod.freelook.option.perspective=Perspektiivi
+sol_client.mod.freelook.option.invertPitch=Käännä hiiri pystysuunnassa
+sol_client.mod.freelook.option.invertYaw=Käännä hiiri vaakatasossa
+
+sol_client.mod.taplook.name=Taplook
+sol_client.mod.taplook.description=Vaihda perspektiivi näppäimen painalluksella.
+
+sol_client.mod.taplook.key=Taplook
+sol_client.mod.taplook.option.key=Näppäin
+sol_client.mod.taplook.option.perspective=Perspektiivi
+
+sol_client.mod.toggle_sprint.name=Juokseminen
+sol_client.mod.toggle_sprint.description=Laita Juokseminen päälle.
+
+sol_client.mod.toggle_sprint.default_width=113
+
+sol_client.mod.toggle_sprint.option.hud=HUD
+
+sol_client.mod.toggle_sprint.held=Juokseminen (Painettu)
+sol_client.mod.toggle_sprint.toggled=Juokseminen (Päällä)
+
+sol_client.mod.zoom.name=Zoomaus
+sol_client.mod.zoom.description=Zoomaa näppäimen painalluksella.
+
+sol_client.mod.zoom.key=Zoomaus
+sol_client.mod.zoom.option.key=Näppäin
+sol_client.mod.zoom.zoom_out=Zoomaa ulos
+sol_client.mod.zoom.option.zoomOutKey=Ulospäin Zoomaamisen näppäin
+sol_client.mod.zoom.zoom_in=Zoomaa sisään
+sol_client.mod.zoom.option.zoomInKey=Sisäänpäin Zoomaamisen näppäin
+sol_client.mod.zoom.option.cinematic=Elokuvakamera
+sol_client.mod.zoom.option.reduceSensitivity=Vähennä herkkyyttä
+sol_client.mod.zoom.option.scrolling=Vieritä Zoomataksesi
+sol_client.mod.zoom.option.smooth=Zoomaus Animaatio
+sol_client.mod.zoom.option.factor=Zoomaustekijä
+
+sol_client.mod.replay.name=Uudelleentoisto
+sol_client.mod.replay.description=Kuvaa ja toista pelaamisesi.
+
+sol_client.mod.replay.option.enableNotifications=Salli ilmoitukset
+sol_client.mod.replay.option.recordSingleplayer=Tallena yksinpeliä
+sol_client.mod.replay.option.recordServer=Tallena palvelinta
+sol_client.mod.replay.option.recordingIndicator=Tallennusilmaisimen
+sol_client.mod.replay.option.recordingIndicatorColour=Tallennusilmaisimen Väri
+sol_client.mod.replay.option.recordingIndicatorScale=Tallennusilmaisimen Koko
+sol_client.mod.replay.option.recordingIndicatorTextColour=Tallennusilmaisimen tekstin väri
+sol_client.mod.replay.option.recordingIndicatorTextShadow=Tallennusilmaisimen tekstin varjo
+sol_client.mod.replay.option.automaticRecording=Automaattinen tallennus
+sol_client.mod.replay.option.renameDialog=Uudelleen nimeä Dialogi
+sol_client.mod.replay.option.showChat=Näytä chat
+
+sol_client.mod.replay.option.camera=Kamera
+sol_client.mod.replay.option.camera.classic=Klassinen
+sol_client.mod.replay.option.camera.vanilla_ish=Vanilla-ish
+
+sol_client.mod.replay.option.showPathPreview=Näytä Polun Esikatselu
+
+sol_client.mod.replay.option.defaultInterpolator=Oletusinterpolaattori
+sol_client.mod.replay.interpolator.catmull=Catmull-Rom Spline
+sol_client.mod.replay.interpolator.cubic=Cubic Spline
+sol_client.mod.replay.interpolator.linear=Lineaarinen
+
+sol_client.mod.replay.option.showServerIPs=Näytä palvelimien IP-osoitteet
+
+sol_client.mod.quickplay.name=Pikapeli
+sol_client.mod.quickplay.description=Liity nopeasti pelien jonoihin näppäimistön avulla.
+
+sol_client.mod.quickplay.key=Pikapeli
+sol_client.mod.quickplay.option.menuKey=Näppäin
+
+sol_client.mod.hypixel_util.name=Hypixel Lisäykset
+sol_client.mod.hypixel_util.description=Useita parannuksia Hypixelin käyttökokemukseen.
+
+sol_client.mod.hypixel_util.option.visitHousingCommand=/visithousing
+sol_client.mod.hypixel_util.option.lobbySoundsVolume=Aulan äänenvoimakkuus
+sol_client.mod.hypixel_util.option.housingMusicVolume=Housing Musiikin Äänenvoimakkuus
+sol_client.mod.hypixel_util.option.popupEvents=Popup Tapahtumat
+sol_client.mod.hypixel_util.option.autogg=Auto GG
+sol_client.mod.hypixel_util.option.autoggMessage=GG viesti
+sol_client.mod.hypixel_util.option.hidegg=Piilota GG
+sol_client.mod.hypixel_util.option.autogl=Auto GL
+sol_client.mod.hypixel_util.option.autoglMessage=GL Viesti
+sol_client.mod.hypixel_util.option.hidegl=Piilota GL
+sol_client.mod.hypixel_util.option.levelhead=Level Head
+
+sol_client.mod.tnt_timer.name=TNT Ajastin
+sol_client.mod.tnt_timer.description=Näytä TNT:n räjähdyksen aika.
+
+sol_client.mod.motion_blur.name=Liikehäivyttäminen
+sol_client.mod.motion_blur.description=Pehmeä Liikehäivyttäminen efekti.
+
+sol_client.mod.motion_blur.option.blur=Sumennus
+
+sol_client.mod.menu_blur.name=Menu Semennus
+sol_client.mod.menu_blur.description=Sumeenna menujen tausta.
+
+sol_client.mod.menu_blur.option.blur=Sumennus
+sol_client.mod.menu_blur.option.fadeTime=Häivytysaika
+sol_client.mod.menu_blur.option.backgroundColour=Taustan Väri
+
+sol_client.mod.colour_saturation.name=Värikylläisyys
+sol_client.mod.colour_saturation.description=Säädä maailman värikylläisyyttä.
+
+sol_client.mod.colour_saturation.option.saturation=Kylläisyys
+
+sol_client.mod.chunk_animator.name=Chunk Animaattori
+sol_client.mod.chunk_animator.description=Animoi maailma sen latautuessa.
+
+sol_client.mod.chunk_animator.option.duration=Kesto
+sol_client.mod.chunk_animator.option.animation=Animaatio
+
+sol_client.mod.1.7_visuals.name=1.7 Visuaalit
+sol_client.mod.1.7_visuals.description=Tuo takaisin hieman 1.7:n näköä ja tunnetta.
+
+sol_client.mod.1.7_visuals.option.useAndMine=Kaiva kun käytät tavaraa
+sol_client.mod.1.7_visuals.option.particles=Hiukkaset
+sol_client.mod.1.7_visuals.option.blocking=Miekan estäminen
+sol_client.mod.1.7_visuals.option.eatingAndDrinking=Syöminen ja Juominen
+sol_client.mod.1.7_visuals.option.rod=Onki
+sol_client.mod.1.7_visuals.option.bow=Jousipyssy
+sol_client.mod.1.7_visuals.option.armourDamage=Vahinkovaikutus panssariin
+sol_client.mod.1.7_visuals.option.sneaking=Pehmeä hiivintä
+
+sol_client.mod.item_physics.name=Tavaroiden Fysiikka
+sol_client.mod.item_physics.description=Lisää esineisiin pyörivä animaatio.
+
+sol_client.mod.item_physics.option.rotationSpeed=Pyörimisnopeus
+
+sol_client.mod.particles.name=Hiukkaset
+sol_client.mod.particles.description=Säädä ja lisää hyökkäyshiukkasia.
+
+sol_client.mod.particles.option.multiplier=Kerroin
+sol_client.mod.particles.option.sharpness=Näytä aina Terävyys
+sol_client.mod.particles.option.snow=Lumi
+sol_client.mod.particles.option.slime=Lima
+sol_client.mod.particles.option.flames=Liekit
+
+sol_client.mod.time_changer.name=Ajan Vaihtaja
+sol_client.mod.time_changer.description=Muuta visuaalista aikaa vaikuttamatta pelaamiseen.
+
+sol_client.mod.time_changer.option.time=Aika
+
+sol_client.mod.block_selection.name=Palikan Valinta
+sol_client.mod.block_selection.description=Mukauta palikan valintaa.
+
+sol_client.mod.block_selection.option.outline=Ääriviivat
+sol_client.mod.block_selection.option.outlineWidth=Ääriviivan Leveys
+sol_client.mod.block_selection.option.outlineColour=Ääriviivan Väri
+sol_client.mod.block_selection.option.fill=Täytä
+sol_client.mod.block_selection.option.fillColour=Täytön VÄri
+sol_client.mod.block_selection.option.depth=Syvyys
+sol_client.mod.block_selection.option.persistent=Pysyvä
+
+sol_client.mod.hit_colour.name=Osuman Väri
+sol_client.mod.hit_colour.description=Muuta osuman väriä.
+
+sol_client.mod.hit_colour.option.colour=Väri
+
+sol_client.mod.hitbox.name=Osuma-alue
+sol_client.mod.hitbox.description=Mukauta ja vaihda helposti entiteetin osuma-aluetta.
+
+sol_client.mod.hitbox.option.toggleHitboxes=Vaihda Hitboxien päälläolo
+sol_client.mod.hitbox.option.boundingBox=Näytä rajoitusruutu
+sol_client.mod.hitbox.option.boundingBoxColour=Rajoitusruudun Väri
+sol_client.mod.hitbox.option.eyeHeight=Näytä silmien korkeus
+sol_client.mod.hitbox.option.eyeHeightColour=Silmien Korkeuden Väri
+sol_client.mod.hitbox.option.lookVector=Näytä katsevektori
+sol_client.mod.hitbox.option.lookVectorColour=Katsevektorin Väri
+sol_client.mod.hitbox.option.lineWidth=Viivan leveys
+
+sol_client.mod.category.general.name=Yleiset
+sol_client.mod.category.hud.name=HUD
+sol_client.mod.category.utility.name=Hyöty
+sol_client.mod.category.visual.name=Visuaaliset
+sol_client.mod.category.integration.name=Integrointi
+
+sol_client.mod.discord_integration.name=Discord Integrointi
+sol_client.mod.discord_integration.description=Discordin integrointiominaisuudet, mukaan lukien aktiviteetit ja peittokuva.
+
+sol_client.mod.discord_integration.option.voiceChatHud=Äänipuhelun HUD
+sol_client.mod.discord_integration.option.voiceChatHudScale=Äänipuhelun HUD Koko
+sol_client.mod.discord_integration.option.voiceChatHudAlignment=Äänipuhelun HUD Linjaus
+sol_client.mod.discord_integration.option.usernameColour=Käyttäjätunnuksen väri
+sol_client.mod.discord_integration.option.mutedColour=Mykistetyn väri
+sol_client.mod.discord_integration.option.speakingColour=Puhuvan väri
+
+sol_client.mod.screen.title=Mods
+sol_client.mod.screen.search=Kirjoita hakeaksesi
+sol_client.mod.screen.apply_to_all=Aseta Kaikkiin
+
+sol_client.hud.edit=Mukauta HUD näyttöä
+
+sol_client.packs.apply=Aseta
+sol_client.packs.open_folder=Paketin Kansio
+sol_client.packs.folder=Kansio (%d tavaraa)
+
+sol_client.screenshot.view=Katso
+sol_client.screenshot.open_folder=Näytä kansiossa
\ No newline at end of file
diff --git a/game/src/main/resources/assets/sol_client/lang/fr_FR.lang b/game/src/main/resources/assets/sol_client/lang/fr_FR.lang
new file mode 100644
index 00000000..550e6917
--- /dev/null
+++ b/game/src/main/resources/assets/sol_client/lang/fr_FR.lang
@@ -0,0 +1,434 @@
+sol_client.passthrough=%s
+sol_client.slider.percent=%s%%
+sol_client.slider.factor=%sx
+sol_client.slider.seconds=%ss
+
+sol_client.vertical_alignment.top=Haut
+sol_client.vertical_alignment.middle=Centre
+sol_client.vertical_alignment.bottom=Fond
+
+sol_client.perspective.first_person=Première personne
+sol_client.perspective.third_person_back=Retour à la troisième personne
+sol_client.perspective.third_person_front=Avant à la troisième personne
+
+sol_client.easing.linear=Linéaire
+sol_client.easing.quad=Quadruple
+sol_client.easing.cubic=Cubique
+sol_client.easing.quart=Quart
+sol_client.easing.quint=Quint
+sol_client.easing.expo=Expo
+sol_client.easing.sine=Sine
+sol_client.easing.circ=Circ
+sol_client.easing.back=Retour
+sol_client.easing.bounce=Rebondir
+sol_client.easing.elastic=Élastique
+
+sol_client.file.edit=Modifier le fichier...
+
+sol_client.servers=Les serveurs
+
+sol_client.key.category=Sol Client
+sol_client.key.accept_request=Accepter la requête
+sol_client.key.dismiss_request=Ignorer la demande
+
+sol_client.mod.generic.option.enabled=Activé
+sol_client.mod.hud.option.scale=Échelle
+sol_client.mod.simple_hud.option.background=Arrière plan
+sol_client.mod.simple_hud.option.backgroundColour=Couleur de l'arrière plan
+sol_client.mod.simple_hud.option.border=Bordure
+sol_client.mod.simple_hud.option.borderColour=Couleur de la bordure
+sol_client.mod.simple_hud.option.textColour=Couleur du texte
+sol_client.mod.simple_hud.option.shadow=Ombre de texte
+
+sol_client.mod.sol_client.name=Sol Client
+sol_client.mod.sol_client.description=Paramètres généraux pour Sol Client.
+
+sol_client.mod.sol_client.option.fancyMainMenu=Menu principal
+sol_client.mod.sol_client.option.logoInInventory=Afficher le logo dans l'inventaire
+sol_client.mod.sol_client.mods=Mods
+sol_client.mod.sol_client.option.modsKey=Clé de mods
+sol_client.mod.sol_client.edit_hud=Modifier l'HUD
+sol_client.mod.sol_client.option.editHudKey=Modifier la clé HUD
+sol_client.mod.sol_client.option.uiColour=Couleur d'accentuation
+sol_client.mod.sol_client.option.smoothUIColours=Couleurs lisses de l'interface utilisateur
+sol_client.mod.sol_client.option.roundedUI=Interface utilisateur arrondie
+sol_client.mod.sol_client.option.buttonClicks=Clics sur les boutons
+sol_client.mod.sol_client.option.smoothScrolling=Défilement fluide
+sol_client.mod.sol_client.option.fancyFont=Police HD
+
+sol_client.mod.fps.name=FPS
+sol_client.mod.fps.description=Affichez les FPS (images par seconde).
+
+sol_client.mod.coordinates.name=Coordonnés
+sol_client.mod.coordinates.description=Affichez vos coordonnées.
+
+sol_client.mod.coordinates.option.axisLabelColour=Couleur de l'étiquette de l'axe
+sol_client.mod.coordinates.option.axisValueColour=Couleur de la valeur de l'axe
+sol_client.mod.coordinates.option.cardinalDirection=Direction cardinale
+sol_client.mod.coordinates.option.cardinalDirectionColour=Couleur de la direction cardinale
+sol_client.mod.coordinates.option.axisDirection=Direction de l'axe
+sol_client.mod.coordinates.option.axisDirectionColour=Couleur de la direction de l'axe
+
+sol_client.mod.keystrokes.name=Frappes
+sol_client.mod.keystrokes.description=Afficher les clés actuellement détenues.
+
+sol_client.mod.keystrokes.option.movement=Mouvement
+sol_client.mod.keystrokes.option.mouse=Souris
+sol_client.mod.keystrokes.option.mouseMovement=Mouvement de la souris
+sol_client.mod.keystrokes.option.showSpace=Espace
+sol_client.mod.keystrokes.option.cps=CPS sur les clés
+sol_client.mod.keystrokes.option.backgroundColourPressed=Couleur de l'arrière plan (maintenu)
+sol_client.mod.keystrokes.option.borderColourPressed=Couleur de la bordure (maintenu)
+sol_client.mod.keystrokes.option.textColourPressed=Couleur du texte (maintenu)
+sol_client.mod.keystrokes.option.smoothColours=Smooth Colors
+
+sol_client.mod.cps.name=CPS
+sol_client.mod.cps.description=Affichez votre CPS (clics par seconde).
+
+sol_client.mod.cps.option.rmb=Bouton droit de la souris CPS
+sol_client.mod.cps.option.separatorColour=Couleur du séparateur
+
+sol_client.mod.ping.name=Ping
+sol_client.mod.ping.description=Affiche la latence sur le serveur actuel.
+
+sol_client.mod.speedometer.name=Compteur de vitesse
+sol_client.mod.speedometer.description=Affichez votre vitesse.
+
+sol_client.mod.speedometer.option.graphMode=Mode graphique
+
+sol_client.mod.reach_display.name=Affichage de la portée
+sol_client.mod.reach_display.description=Affichez la distance lorsque vous frappez des entités.
+
+sol_client.mod.combo_counter.name=Compteur combiné
+sol_client.mod.combo_counter.description=Comptez le nombre de hits suivants.
+
+sol_client.mod.combo_counter.no_hits=Pas de combo
+sol_client.mod.combo_counter.one_hit=1 hit
+sol_client.mod.combo_counter.n_hits=%s hits
+
+sol_client.mod.potion_effects.name=Effets des potions
+sol_client.mod.potion_effects.description=Affichez vos potions actives avec des comptes à rebours.
+
+sol_client.mod.potion_effects.option.alignment=Alignement
+sol_client.mod.potion_effects.option.icon=Icône
+sol_client.mod.potion_effects.option.background=Arrière plan
+sol_client.mod.potion_effects.option.title=Étiquette
+sol_client.mod.potion_effects.option.titleColour=Couleur de l'étiquette
+sol_client.mod.potion_effects.option.duration=Durée
+sol_client.mod.potion_effects.option.durationColour=Durée Couleur
+sol_client.mod.potion_effects.option.spacing=Espacement
+
+sol_client.mod.armour.name=Armure et équipement
+sol_client.mod.armour.description=Affichez votre armure et votre équipement avec durabilité.
+
+sol_client.mod.armour.option.durability=Durabilité
+sol_client.mod.armour.option.durability.remaining=Restant
+sol_client.mod.armour.option.durability.percentage=Pourcentage
+sol_client.mod.armour.option.durability.fraction=Fraction
+
+sol_client.mod.armour.option.armour=Afficher l'armure
+sol_client.mod.armour.option.hand=Afficher l'élément
+
+sol_client.mod.timers.name=Minuteries
+sol_client.mod.timers.description=Afficher les minuteries pour les événements en jeu.
+
+sol_client.mod.timers.option.alignment=Alignement
+sol_client.mod.timers.option.icon=Icône
+sol_client.mod.timers.option.nameColour=Couleur de l'étiquette
+sol_client.mod.timers.option.timeColour=Couleur de l'heure
+
+sol_client.mod.chat.name=Discuter
+sol_client.mod.chat.description=Personnalisez le Discuter en jeu.
+
+sol_client.mod.chat.peek=Aperçu du chat
+sol_client.mod.chat.option.preventClose=Empêcher la fermeture
+sol_client.mod.chat.option.smooth=Animations de messages
+sol_client.mod.chat.option.infiniteChat=Chat infini
+sol_client.mod.chat.option.peekKey=Clé d'aperçu
+
+sol_client.mod.chat.option.visibility=Visibilité
+sol_client.mod.chat.option.visibility.shown=Montré
+sol_client.mod.chat.option.visibility.commands=Commandes uniquement
+sol_client.mod.chat.option.visibility.hidden=Caché
+
+sol_client.mod.chat.option.defaultTextColour=Couleur du texte par défaut
+sol_client.mod.chat.option.colours=Couleurs
+sol_client.mod.chat.option.width=Largeur
+sol_client.mod.chat.option.closedHeight=Hauteur fermée
+sol_client.mod.chat.option.openHeight=Hauteur ouverte
+sol_client.mod.chat.option.links=Liens
+sol_client.mod.chat.option.promptLinks=Liens rapides
+sol_client.mod.chat.option.chatFilter=Activer le filtre de discussion
+sol_client.mod.chat.option.filteredWordsContent=Mots filtrés
+
+sol_client.mod.tab_list.name=Liste des onglets
+sol_client.mod.tab_list.description=Personnalisez la liste des onglets/joueurs.
+
+sol_client.mod.tab_list.option.hideHeader=Masquer l'en-tête
+sol_client.mod.tab_list.option.hideFooter=Masquer le pied de page
+
+sol_client.mod.tab_list.option.pingType=Ping
+sol_client.mod.tab_list.option.pingType.none=Caché
+sol_client.mod.tab_list.option.pingType.icon=Icône
+sol_client.mod.tab_list.option.pingType.numeral=Numéro
+
+sol_client.mod.tab_list.option.backgroundColour=Couleur d'arrière-plan de la liste
+sol_client.mod.tab_list.option.entryBackgroundColour=Couleur d'arrière-plan de l'entrée
+sol_client.mod.tab_list.option.hidePlayerHeads=Masquer les têtes des joueurs
+sol_client.mod.tab_list.option.textShadow=Ombre de texte
+
+sol_client.mod.crosshair.name=Réticule
+sol_client.mod.crosshair.description=Personnalisez le réticule.
+
+sol_client.mod.crosshair.option.style=Style
+sol_client.mod.crosshair.option.style.default=Défaut
+sol_client.mod.crosshair.option.style.none=Aucun
+sol_client.mod.crosshair.option.style.dot=Point
+sol_client.mod.crosshair.option.style.plus=Plus
+sol_client.mod.crosshair.option.style.plus_dot=Plus point
+sol_client.mod.crosshair.option.style.square=Carré
+sol_client.mod.crosshair.option.style.square_dot=Point carré
+sol_client.mod.crosshair.option.style.circle=Cercle
+sol_client.mod.crosshair.option.style.circle_dot=Point de cercle
+sol_client.mod.crosshair.option.style.four_angled=4 Coudé
+sol_client.mod.crosshair.option.style.four_angled_dot=4 points inclinés
+sol_client.mod.crosshair.option.style.triangle=Triangle
+
+sol_client.mod.crosshair.option.thirdPerson=Afficher à la troisième personne
+sol_client.mod.crosshair.option.spectatorAlways=Afficher en mode spectateur
+sol_client.mod.crosshair.option.debug=Afficher dans la superposition de débogage
+sol_client.mod.crosshair.option.blending=Mélange
+sol_client.mod.crosshair.option.crosshairColour=Couleur
+sol_client.mod.crosshair.option.highlightEntities=Mettre en surbrillance les entités
+sol_client.mod.crosshair.option.entityColour=Couleur de l'entité
+
+sol_client.mod.scoreboard.name=Tableau de bord
+sol_client.mod.scoreboard.description=Personnalisez le tableau de bord.
+
+sol_client.mod.scoreboard.option.hide=cacher
+sol_client.mod.scoreboard.option.backgroundColourTop=Couleur d'arrière-plan du haut
+sol_client.mod.scoreboard.option.numbers=Nombres
+sol_client.mod.scoreboard.option.numbersColour=Couleur des chiffres
+
+sol_client.mod.tweaks.name=Ajustements
+sol_client.mod.tweaks.description=Divers réglages de jeu, y compris fullbright.
+
+sol_client.mod.tweaks.option.fullbright=Très brillant
+sol_client.mod.tweaks.option.showOwnTag=Afficher l'étiquette de son propre nom
+sol_client.mod.tweaks.option.arabicNumerals=chiffres arabes
+sol_client.mod.tweaks.option.betterTooltips=De meilleures info-bulles
+sol_client.mod.tweaks.option.minimalViewBobbing=Bobbing de vue minimal
+sol_client.mod.tweaks.option.minimalDamageShake=Dégâts minimaux
+sol_client.mod.tweaks.option.damageShakeIntensity=Dégâts Shake Intensity
+sol_client.mod.tweaks.option.confirmDisconnect=Confirmer la déconnexion
+sol_client.mod.tweaks.option.borderlessFullscreen=Plein écran sans bordure
+sol_client.mod.tweaks.option.betterKeyBindings=Meilleures liaisons de clé
+sol_client.mod.tweaks.option.disableHotbarScrolling=Désactiver le défilement de la barre de raccourcis
+
+sol_client.mod.tweaks.confirm_disconnect=Appuyez à nouveau pour confirmer
+
+sol_client.mod.freelook.name=Regard libre
+sol_client.mod.freelook.description=Déverrouillez la rotation de la caméra depuis la tête du joueur.
+
+sol_client.mod.freelook.key=Regard libre
+sol_client.mod.freelook.option.key=Clé
+
+sol_client.mod.freelook.option.perspective=Perspective
+sol_client.mod.freelook.option.invertPitch=Inverser la souris verticalement
+sol_client.mod.freelook.option.invertYaw=Inverser la souris horizontalement
+
+sol_client.mod.taplook.name=Appuyez sur regarder
+sol_client.mod.taplook.description=Changez votre perspective en maintenant un bouton.
+
+sol_client.mod.taplook.key=Appuyez sur regarder
+sol_client.mod.taplook.option.key=Clé
+sol_client.mod.taplook.option.perspective=Perspective
+
+sol_client.mod.toggle_sprint.name=Basculer le sprint
+sol_client.mod.toggle_sprint.description=Basculez la touche de sprint.
+
+sol_client.mod.toggle_sprint.default_width=113
+
+sol_client.mod.toggle_sprint.option.hud=HUD
+
+sol_client.mod.toggle_sprint.held=Sprint (tenu)
+sol_client.mod.toggle_sprint.toggled=Sprint (basculé)
+
+sol_client.mod.zoom.name=Zoom
+sol_client.mod.zoom.description=Zoom avant en appuyant sur un bouton.
+
+sol_client.mod.zoom.key=Zoom
+sol_client.mod.zoom.option.key=Clé
+sol_client.mod.zoom.zoom_out=Dézoomer
+sol_client.mod.zoom.option.zoomOutKey=Touche de zoom arrière
+sol_client.mod.zoom.zoom_in=agrandir
+sol_client.mod.zoom.option.zoomInKey=Touche Zoom avant
+sol_client.mod.zoom.option.cinematic=Caméra cinématographique
+sol_client.mod.zoom.option.reduceSensitivity=Réduire la sensibilité
+sol_client.mod.zoom.option.scrolling=Faites défiler pour zoomer
+sol_client.mod.zoom.option.smooth=Zoomer les animations
+sol_client.mod.zoom.option.factor=Facteur de zoom
+
+sol_client.mod.replay.name=Rejouer
+sol_client.mod.replay.description=Enregistrez et rejouez votre gameplay.
+
+sol_client.mod.replay.option.enableNotifications=Activer les notifications
+sol_client.mod.replay.option.recordSingleplayer=Enregistrer un joueur
+sol_client.mod.replay.option.recordServer=Serveur d'enregistrement
+sol_client.mod.replay.option.recordingIndicator=Indicateur d'enregistrement
+sol_client.mod.replay.option.recordingIndicatorColour=Couleur de l'indicateur d'enregistrement
+sol_client.mod.replay.option.recordingIndicatorScale=Échelle de l'indicateur d'enregistrement
+sol_client.mod.replay.option.recordingIndicatorTextColour=Couleur du texte de l'indicateur d'enregistrement
+sol_client.mod.replay.option.recordingIndicatorTextShadow=Ombre du texte de l'indicateur d'enregistrement
+sol_client.mod.replay.option.automaticRecording=Enregistrement automatique
+sol_client.mod.replay.option.renameDialog=Boîte de dialogue Renommer
+sol_client.mod.replay.option.showChat=Afficher la conversation
+
+sol_client.mod.replay.option.camera=Caméra
+sol_client.mod.replay.option.camera.classic=Classique
+sol_client.mod.replay.option.camera.vanilla_ish=À la vanille
+
+sol_client.mod.replay.option.showPathPreview=Afficher l'aperçu du chemin
+
+sol_client.mod.replay.option.defaultInterpolator=Interpolateur par défaut
+sol_client.mod.replay.interpolator.catmull=Spline Catmull-Rom
+sol_client.mod.replay.interpolator.cubic=Spline cubique
+sol_client.mod.replay.interpolator.linear=Linéaire
+
+sol_client.mod.replay.option.showServerIPs=Afficher l'IP du serveur
+
+sol_client.mod.quickplay.name=Jeu rapide
+sol_client.mod.quickplay.description=Mettez rapidement les jeux en file d'attente à l'aide de votre clavier.
+
+sol_client.mod.quickplay.key=Jeu rapide
+sol_client.mod.quickplay.option.menuKey=Clé
+
+sol_client.mod.hypixel_util.name=Ajouts d'Hypixels
+sol_client.mod.hypixel_util.description=Diverses améliorations de l'expérience sur Hypixel.
+
+sol_client.mod.hypixel_util.option.visitHousingCommand=/visiterlelogement
+sol_client.mod.hypixel_util.option.lobbySoundsVolume=Volume des sons du hall
+sol_client.mod.hypixel_util.option.housingMusicVolume=Volume de la musique du logement
+sol_client.mod.hypixel_util.option.popupEvents=Événements contextuels
+sol_client.mod.hypixel_util.option.autogg=GG automatique
+sol_client.mod.hypixel_util.option.autoggMessage=Message GG
+sol_client.mod.hypixel_util.option.hidegg=Masquer GG
+sol_client.mod.hypixel_util.option.autogl=GL automatique
+sol_client.mod.hypixel_util.option.autoglMessage=Message GL
+sol_client.mod.hypixel_util.option.hidegl=Masquer GL
+sol_client.mod.hypixel_util.option.levelhead=Tête de niveau
+
+sol_client.mod.tnt_timer.name=Minuterie TNT
+sol_client.mod.tnt_timer.description=Afficher le temps d'explosion de TNT.
+
+sol_client.mod.motion_blur.name=Flou de mouvement
+sol_client.mod.motion_blur.description=Effet de flou de mouvement fluide.
+
+sol_client.mod.motion_blur.option.blur=Se brouiller
+
+sol_client.mod.menu_blur.name=Menu flou
+sol_client.mod.menu_blur.description=Flou l'arrière-plan des menus.
+
+sol_client.mod.menu_blur.option.blur=Se brouiller
+sol_client.mod.menu_blur.option.fadeTime=Temps de fondu
+sol_client.mod.menu_blur.option.backgroundColour=Couleur de l'arrière plan
+
+sol_client.mod.colour_saturation.name=Saturation de couleur
+sol_client.mod.colour_saturation.description=Ajustez la saturation des couleurs du monde.
+
+sol_client.mod.colour_saturation.option.saturation=Saturation
+
+sol_client.mod.chunk_animator.name=Animateur de morceaux
+sol_client.mod.chunk_animator.description=Animez le monde pendant qu'il se charge.
+
+sol_client.mod.chunk_animator.option.duration=Durée
+sol_client.mod.chunk_animator.option.animation=Animation
+
+sol_client.mod.1.7_visuals.name=1.7 Visuels
+sol_client.mod.1.7_visuals.description=Ramenez une partie de l'aspect et de la convivialité de 1.7.
+
+sol_client.mod.1.7_visuals.option.useAndMine=Creusez en utilisant l'objet
+sol_client.mod.1.7_visuals.option.particles=Particules
+sol_client.mod.1.7_visuals.option.blocking=Blocage de l'épée
+sol_client.mod.1.7_visuals.option.eatingAndDrinking=Manger et boire
+sol_client.mod.1.7_visuals.option.rod=Canne à pêche
+sol_client.mod.1.7_visuals.option.bow=Arc
+sol_client.mod.1.7_visuals.option.armourDamage=Effet des dégâts sur l'armure
+sol_client.mod.1.7_visuals.option.sneaking=Se faufiler en douceur
+
+sol_client.mod.item_physics.name=Physique de l'objet
+sol_client.mod.item_physics.description=Ajoutez une animation tournante aux éléments.
+
+sol_client.mod.item_physics.option.rotationSpeed=Vitesse de rotation
+
+sol_client.mod.particles.name=Particules
+sol_client.mod.particles.description=Ajustez et multipliez les particules d'attaque.
+
+sol_client.mod.particles.option.multiplier=Multiplicateur
+sol_client.mod.particles.option.sharpness=Toujours afficher la netteté
+sol_client.mod.particles.option.snow=Neiger
+sol_client.mod.particles.option.slime=Vase
+sol_client.mod.particles.option.flames=Flammes
+
+sol_client.mod.time_changer.name=Changeur d'heure
+sol_client.mod.time_changer.description=Changez le temps visuel sans affecter le gameplay.
+
+sol_client.mod.time_changer.option.time=Temps
+
+sol_client.mod.block_selection.name=Sélection de bloc
+sol_client.mod.block_selection.description=Personnalisez la sélection de blocs.
+
+sol_client.mod.block_selection.option.outline=Présenter
+sol_client.mod.block_selection.option.outlineWidth=Largeur du contour
+sol_client.mod.block_selection.option.outlineColour=Couleur du contour
+sol_client.mod.block_selection.option.fill=Remplir
+sol_client.mod.block_selection.option.fillColour=La couleur de remplissage
+sol_client.mod.block_selection.option.depth=Profondeur
+sol_client.mod.block_selection.option.persistent=Persistant
+
+sol_client.mod.hit_colour.name=Frapper la couleur
+sol_client.mod.hit_colour.description=Changez la couleur du coup.
+
+sol_client.mod.hit_colour.option.colour=Couleur
+
+sol_client.mod.hitbox.name=Hitbox
+sol_client.mod.hitbox.description=Personnalisez et basculez facilement les affichages de la hitbox des entités.
+
+sol_client.mod.hitbox.option.toggleHitboxes=Basculer les hitbox
+sol_client.mod.hitbox.option.boundingBox=Afficher la boîte englobante
+sol_client.mod.hitbox.option.boundingBoxColour=Couleur de la boîte englobante
+sol_client.mod.hitbox.option.eyeHeight=Afficher la hauteur des yeux
+sol_client.mod.hitbox.option.eyeHeightColour=Couleur de la hauteur des yeux
+sol_client.mod.hitbox.option.lookVector=Afficher le vecteur de look
+sol_client.mod.hitbox.option.lookVectorColour=Regardez la couleur du vecteur
+sol_client.mod.hitbox.option.lineWidth=Largeur de ligne
+
+sol_client.mod.category.general.name=Général
+sol_client.mod.category.hud.name=HUD
+sol_client.mod.category.utility.name=Utilitaire
+sol_client.mod.category.visual.name=Visuel
+sol_client.mod.category.integration.name=l'intégration
+
+sol_client.mod.discord_integration.name=Intégration Discord
+sol_client.mod.discord_integration.description=Fonctionnalités d'intégration pour Discord, y compris les activités et la superposition.
+
+sol_client.mod.discord_integration.option.voiceChatHud=HUD de chat vocal
+sol_client.mod.discord_integration.option.voiceChatHudScale=Échelle HUD de chat vocal
+sol_client.mod.discord_integration.option.voiceChatHudAlignment=Alignement du HUD du chat vocal
+sol_client.mod.discord_integration.option.usernameColour=Couleur du nom d'utilisateur
+sol_client.mod.discord_integration.option.mutedColour=Couleur en sourdine
+sol_client.mod.discord_integration.option.speakingColour=Couleur parlante
+
+sol_client.mod.screen.title=Mods
+sol_client.mod.screen.search=Tapez pour rechercher
+sol_client.mod.screen.apply_to_all=S'applique à tous
+
+
+sol_client.hud.edit=Modifier l'HUD
+
+sol_client.packs.apply=Appliquer
+sol_client.packs.open_folder=Dossier de pack
+sol_client.packs.folder=Dossier (%d items)
+
+sol_client.screenshot.view=Voir
+sol_client.screenshot.open_folder=Afficher dans le dossier
diff --git a/game/src/main/resources/assets/sol_client/lang/vi_VN.lang b/game/src/main/resources/assets/sol_client/lang/vi_VN.lang
new file mode 100644
index 00000000..f320a6c1
--- /dev/null
+++ b/game/src/main/resources/assets/sol_client/lang/vi_VN.lang
@@ -0,0 +1,439 @@
+sol_client.passthrough=%s
+sol_client.slider.percent=%s%%
+sol_client.slider.factor=%sx
+sol_client.slider.seconds=%ss
+
+sol_client.vertical_alignment.top=Trên
+sol_client.vertical_alignment.middle=Giữa
+sol_client.vertical_alignment.bottom=Cuối
+
+sol_client.perspective.first_person=Góc nhìn thứ nhất
+sol_client.perspective.third_person_back=Góc nhìn thứ 3 phía sau
+sol_client.perspective.third_person_front=Góc nhìn thứ 3 phía trước
+
+sol_client.easing.linear=Linear
+sol_client.easing.quad=Quad
+sol_client.easing.cubic=Cubic
+sol_client.easing.quart=Quart
+sol_client.easing.quint=Quint
+sol_client.easing.expo=Expo
+sol_client.easing.sine=Sine
+sol_client.easing.circ=Circ
+sol_client.easing.back=Back
+sol_client.easing.bounce=Bounce
+sol_client.easing.elastic=Elastic
+
+sol_client.file.edit=Chỉnh sửa tập tin...
+
+sol_client.servers=Danh sách máy chủ
+
+sol_client.key.category=Sol Client
+sol_client.key.accept_request=Chấp nhận yêu cầu
+sol_client.key.dismiss_request=Từ chối yêu cầu
+
+sol_client.mod.generic.option.enabled=Bật
+sol_client.mod.hud.option.scale=Độ lớn
+sol_client.mod.simple_hud.option.background=Nền
+sol_client.mod.simple_hud.option.backgroundColour=Màu nền
+sol_client.mod.simple_hud.option.border=Viền
+sol_client.mod.simple_hud.option.borderColour=Màu viền
+sol_client.mod.simple_hud.option.textColour=Màu chữ
+sol_client.mod.simple_hud.option.shadow=Bóng chữ
+sol_client.mod.smooth_counter_hud.option.smoothNumbers=Hiệu ứng nhảy số
+
+sol_client.mod.sol_client.name=Sol Client
+sol_client.mod.sol_client.description=Các thiết lập chung của Sol Client.
+
+sol_client.mod.sol_client.option.fancyMainMenu=Sử dụng Màn hình chính của Sol
+sol_client.mod.sol_client.option.logoInInventory=Hiện Logo ở giao diện Túi đồ
+sol_client.mod.sol_client.mods=Mods
+sol_client.mod.sol_client.option.modsKey=Phím tắt mở Danh sách Mod
+sol_client.mod.sol_client.edit_hud=Chỉnh sửa HUD
+sol_client.mod.sol_client.option.editHudKey=Chỉnh sửa phím tắt HUD
+sol_client.mod.sol_client.option.uiColour=Màu sắc chủ đạo
+sol_client.mod.sol_client.option.smoothUIColours=Làm mượt màu sắc chủ đạo
+sol_client.mod.sol_client.option.roundedUI=Giao diện bo tròn
+sol_client.mod.sol_client.option.buttonClicks=Âm thanh khi nhấn nút
+sol_client.mod.sol_client.option.smoothScrolling=Làm mượt cuộn trang
+sol_client.mod.sol_client.option.fancyFont=Phông chữ HD
+
+sol_client.mod.fps.name=FPS
+sol_client.mod.fps.description=Hiện FPS (Số lượng khung hình mỗi giây).
+
+sol_client.mod.coordinates.name=Toạ độ
+sol_client.mod.coordinates.description=Hiện toạ độ của bạn.
+
+sol_client.mod.coordinates.option.axisLabelColour=Màu tiêu đề của toạ độ
+sol_client.mod.coordinates.option.axisValueColour=Màu giá trị toạ độ
+sol_client.mod.coordinates.option.cardinalDirection=Hướng đang nhìn
+sol_client.mod.coordinates.option.cardinalDirectionColour=Màu hướng đang nhìn
+sol_client.mod.coordinates.option.axisDirection=Hướng thay đổi giá trị
+sol_client.mod.coordinates.option.axisDirectionColour=Màu hướng thay đổi giá trị
+
+sol_client.mod.keystrokes.name=Keystrokes
+sol_client.mod.keystrokes.description=Hiển thị phím đang được ấn.
+
+sol_client.mod.keystrokes.option.movement=Di chuyển
+sol_client.mod.keystrokes.option.mouse=Chuột
+sol_client.mod.keystrokes.option.mouseMovement=Di chuyển chuột
+sol_client.mod.keystrokes.option.showSpace=Dấu cách
+sol_client.mod.keystrokes.option.cps=Hiện CPS cho phím
+sol_client.mod.keystrokes.option.backgroundColourPressed=Màu nền (Khi ấn)
+sol_client.mod.keystrokes.option.borderColourPressed=Màu viền (Khi ấn)
+sol_client.mod.keystrokes.option.textColourPressed=Màu chữ (Khi ấn)
+sol_client.mod.keystrokes.option.smoothColours=Làm mượt màu sắc
+
+sol_client.mod.cps.name=CPS
+sol_client.mod.cps.description=Hiển thị CPS (Số lần click mỗi giây).
+
+sol_client.mod.cps.option.rmb=CPS chuột phải
+sol_client.mod.cps.option.separatorColour=Màu phân cách
+
+sol_client.mod.ping.name=Ping
+sol_client.mod.ping.description=Hiển thị độ trễ của kết nối đến máy chủ.
+
+sol_client.mod.ping.option.source=Nguồn thông tin
+sol_client.mod.ping.option.source.auto=Tự động
+sol_client.mod.ping.option.source.multiplayer_screen=Ở màn hình Chọn máy chủ
+sol_client.mod.ping.option.source.tab_list=Ở Tab
+
+sol_client.mod.speedometer.name=Bộ đếm tốc độ
+sol_client.mod.speedometer.description=Hiển thị tốc độ di chuyển.
+
+sol_client.mod.speedometer.option.graphMode=Chế độ đồ thị
+
+sol_client.mod.reach_display.name=Reach Display
+sol_client.mod.reach_display.description=Hiển thị khoảng cách khi PVP.
+
+sol_client.mod.combo_counter.name=Bộ đếm Combo
+sol_client.mod.combo_counter.description=Hiển thị số lần đánh trúng liên tiếp.
+
+sol_client.mod.combo_counter.no_hits=Chưa đánh trúng
+sol_client.mod.combo_counter.one_hit=1 lần đánh trúng liên tục
+sol_client.mod.combo_counter.n_hits=%s lần đánh trúng liên tục
+
+sol_client.mod.potion_effects.name=Hiệu ứng thuốc
+sol_client.mod.potion_effects.description=Hiển thị hiệu ứng thuốc đang có hiệu lực và thời gian còn lại.
+
+sol_client.mod.potion_effects.option.alignment=Căn lề
+sol_client.mod.potion_effects.option.icon=Biểu tượng
+sol_client.mod.potion_effects.option.background=Nền
+sol_client.mod.potion_effects.option.title=Tên thuốc
+sol_client.mod.potion_effects.option.titleColour=Màu chữ
+sol_client.mod.potion_effects.option.duration=Thời gian còn lại
+sol_client.mod.potion_effects.option.durationColour=Màu thời gian
+sol_client.mod.potion_effects.option.spacing=Khoảng cách giữa các hiệu ứng
+
+sol_client.mod.armour.name=Giáp và công cụ
+sol_client.mod.armour.description=Hiển thị giáp và công cụ cùng với độ bền hiện tại.
+
+sol_client.mod.armour.option.durability=Độ bền
+sol_client.mod.armour.option.durability.remaining=Còn lại
+sol_client.mod.armour.option.durability.percentage=Phần trăm
+sol_client.mod.armour.option.durability.fraction=Phân số
+
+sol_client.mod.armour.option.armour=Hiển thị giáp đang mặc
+sol_client.mod.armour.option.hand=Hiển thị vật phẩm đang cầm trên tay
+
+sol_client.mod.timers.name=Bộ đếm thời gian sự kiện
+sol_client.mod.timers.description=Hiển thị thời gian còn lại của các sự kiện.
+
+sol_client.mod.timers.option.alignment=Căn lề
+sol_client.mod.timers.option.icon=Biểu tượng
+sol_client.mod.timers.option.nameColour=Màu chữ
+sol_client.mod.timers.option.timeColour=Màu thời gian
+
+sol_client.mod.chat.name=Trò chuyện
+sol_client.mod.chat.description=Tuỳ chỉnh khung trò chuyện.
+
+sol_client.mod.chat.peek=Mở trò chuyện nhanh
+sol_client.mod.chat.option.preventClose=Ngăn việc đóng khung trò chuyện tự động
+sol_client.mod.chat.option.smooth=Hiệu ứng chat chạy lên khi có trò chuyện mới
+sol_client.mod.chat.option.infiniteChat=Lịch sử trò chuyện không giới hạn
+sol_client.mod.chat.option.peekKey=Phím tắt đọc trò chuyện nhanh
+
+sol_client.mod.chat.option.visibility=Hiển thị trò chuyện
+sol_client.mod.chat.option.visibility.shown=Mở
+sol_client.mod.chat.option.visibility.commands=Chỉ khi nhập lệnh
+sol_client.mod.chat.option.visibility.hidden=Đóng
+
+sol_client.mod.chat.option.defaultTextColour=Màu chữ mặc định
+sol_client.mod.chat.option.colours=Màu sắc
+sol_client.mod.chat.option.width=Độ rộng
+sol_client.mod.chat.option.closedHeight=Chiều cao chat khi đóng
+sol_client.mod.chat.option.openHeight=Chiều cao chat khi mở
+sol_client.mod.chat.option.links=Liên kết
+sol_client.mod.chat.option.promptLinks=Nhắc nhở khi ấn vào liên kết
+sol_client.mod.chat.option.chatFilter=Bật bộ lọc chat
+sol_client.mod.chat.option.filteredWordsContent=Danh sách các từ bị lọc
+
+sol_client.mod.tab_list.name=Tab List
+sol_client.mod.tab_list.description=Tuỳ chỉnh tab/danh sách người chơi.
+
+sol_client.mod.tab_list.option.hideHeader=Ẩn Header
+sol_client.mod.tab_list.option.hideFooter=Ẩn Footer
+
+sol_client.mod.tab_list.option.pingType=Ping
+sol_client.mod.tab_list.option.pingType.none=Ẩn
+sol_client.mod.tab_list.option.pingType.icon=Biểu tượng
+sol_client.mod.tab_list.option.pingType.numeral=Số
+
+sol_client.mod.tab_list.option.backgroundColour=Màu nền chung
+sol_client.mod.tab_list.option.entryBackgroundColour=Màu nền từng dòng
+sol_client.mod.tab_list.option.hidePlayerHeads=Ẩn biểu tượng đầu người chơi
+sol_client.mod.tab_list.option.textShadow=Bóng đổ chữ
+
+sol_client.mod.crosshair.name=Hồng tâm
+sol_client.mod.crosshair.description=Tuỳ chỉnh hồng tâm.
+
+sol_client.mod.crosshair.option.style=Kiểu hồng tâm.
+sol_client.mod.crosshair.option.style.default=Mặc định
+sol_client.mod.crosshair.option.style.none=Ẩn hoàn toàn
+sol_client.mod.crosshair.option.style.dot=Chấm
+sol_client.mod.crosshair.option.style.plus=Dấu cộng
+sol_client.mod.crosshair.option.style.plus_dot=Dấu cộng chấm
+sol_client.mod.crosshair.option.style.square=Hình vuông
+sol_client.mod.crosshair.option.style.square_dot=Hình vuông chấm
+sol_client.mod.crosshair.option.style.circle=Hình tròn
+sol_client.mod.crosshair.option.style.circle_dot=Hình tròn chấm
+sol_client.mod.crosshair.option.style.four_angled=Gạch chéo
+sol_client.mod.crosshair.option.style.four_angled_dot=Gạch chéo chấm
+sol_client.mod.crosshair.option.style.triangle=Tam giác
+
+sol_client.mod.crosshair.option.thirdPerson=Hiển thị ở góc nhìn thứ 3
+sol_client.mod.crosshair.option.spectatorAlways=Hiển thị ở chế độ Khán giả
+sol_client.mod.crosshair.option.debug=Hiển thị ở Màn hình Gỡ rối
+sol_client.mod.crosshair.option.blending=Hoà trộn màu
+sol_client.mod.crosshair.option.crosshairColour=Màu hồng tâm
+sol_client.mod.crosshair.option.highlightEntities=Đổi màu khi đưa vào thực thể
+sol_client.mod.crosshair.option.entityColour=Màu khi đưa vào thực thể
+
+sol_client.mod.scoreboard.name=Scoreboard
+sol_client.mod.scoreboard.description=Tuỳ chỉnh scoreboard.
+
+sol_client.mod.scoreboard.option.hide=Tắt scoreboard
+sol_client.mod.scoreboard.option.backgroundColourTop=Màu tiêu đề
+sol_client.mod.scoreboard.option.numbers=Hiển thị số ở cuối hàng
+sol_client.mod.scoreboard.option.numbersColour=Màu số
+
+sol_client.mod.tweaks.name=Tinh chỉnh khác
+sol_client.mod.tweaks.description=Các tinh chỉnh khác, bao gồm fullbright.
+
+sol_client.mod.tweaks.option.fullbright=Fullbright
+sol_client.mod.tweaks.option.showOwnTag=Hiển thị tên của chính mình
+sol_client.mod.tweaks.option.arabicNumerals=Sử dụng Số La Mã
+sol_client.mod.tweaks.option.betterTooltips=Chú giải vật phẩm tốt hơn
+sol_client.mod.tweaks.option.minimalViewBobbing=Hạn chế lay động tầm nhìn
+sol_client.mod.tweaks.option.minimalDamageShake=Hạn chế rung màn hình khi nhận sát thương
+sol_client.mod.tweaks.option.damageShakeIntensity=Độ rung màn hình khi nhận sát thương
+sol_client.mod.tweaks.option.confirmDisconnect=Xác nhận khi thoát khỏi máy chủ
+sol_client.mod.tweaks.option.borderlessFullscreen=Toàn màn hình không viền
+sol_client.mod.tweaks.option.betterKeyBindings=Nhận diện phím tắt tốt hơn
+sol_client.mod.tweaks.option.disableHotbarScrolling=Vô hiệu hoá thanh cuộn Hotbar
+
+sol_client.mod.tweaks.confirm_disconnect=Ấn một lần nữa để thoát khỏi máy chủ
+
+sol_client.mod.freelook.name=Freelook
+sol_client.mod.freelook.description=Tự do nhìn xung quanh.
+
+sol_client.mod.freelook.key=Freelook
+sol_client.mod.freelook.option.key=Phím tắt
+
+sol_client.mod.freelook.option.perspective=Hướng nhìn
+sol_client.mod.freelook.option.invertPitch=Đảo ngược hướng dọc của chuột
+sol_client.mod.freelook.option.invertYaw=Đảo ngược hướng ngang của chuột
+
+sol_client.mod.taplook.name=Taplook
+sol_client.mod.taplook.description=Chuyển đổi góc nhìn nhanh bằng phím tắt.
+
+sol_client.mod.taplook.key=Taplook
+sol_client.mod.taplook.option.key=Phím tắt
+sol_client.mod.taplook.option.perspective=Góc nhìn khi ấn
+
+sol_client.mod.toggle_sprint.name=Toggle Sprint
+sol_client.mod.toggle_sprint.description=Khoá phím chạy.
+
+sol_client.mod.toggle_sprint.default_width=113
+
+sol_client.mod.toggle_sprint.option.hud=HUD
+
+sol_client.mod.toggle_sprint.held=Đang chạy (Giữ)
+sol_client.mod.toggle_sprint.toggled=Đang chạy (Khoá phím)
+
+sol_client.mod.zoom.name=Phóng to
+sol_client.mod.zoom.description=Phóng to màn hình
+
+sol_client.mod.zoom.key=Phóng to
+sol_client.mod.zoom.option.key=Phím tắt
+sol_client.mod.zoom.zoom_out=Thu nhỏ
+sol_client.mod.zoom.option.zoomOutKey=Phím tắt thu nhỏ
+sol_client.mod.zoom.zoom_in=Phóng to
+sol_client.mod.zoom.option.zoomInKey=Phím tắt phóng to
+sol_client.mod.zoom.option.cinematic=Camera kịch tính
+sol_client.mod.zoom.option.reduceSensitivity=Giảm tốc độ chuột
+sol_client.mod.zoom.option.scrolling=Thay đổi độ lớn bằng cuộn chuột
+sol_client.mod.zoom.option.smooth=Hiệu ứng thu phóng
+sol_client.mod.zoom.option.factor=Độ lớn thu phóng
+
+sol_client.mod.replay.name=Phát lại
+sol_client.mod.replay.description=Ghi lại diễn biến trò chơi và xem lại khi bạn muốn.
+
+sol_client.mod.replay.option.enableNotifications=Thông báo hoạt động
+sol_client.mod.replay.option.recordSingleplayer=Ghi hình chơi đơn
+sol_client.mod.replay.option.recordServer=Ghi hình chơi nhiều người
+sol_client.mod.replay.option.recordingIndicator=Hiển thị Chỉ báo Ghi hình
+sol_client.mod.replay.option.recordingIndicatorColour=Màu sắc Chỉ báo
+sol_client.mod.replay.option.recordingIndicatorScale=Kích thước Chỉ báo
+sol_client.mod.replay.option.recordingIndicatorTextColour=Màu sắc chữ
+sol_client.mod.replay.option.recordingIndicatorTextShadow=Bóng đổ chữ
+sol_client.mod.replay.option.automaticRecording=Tự động bắt đầu ghi hình
+sol_client.mod.replay.option.renameDialog=Cửa sổ đổi tên bản ghi khi hoàn tất ghi hình
+sol_client.mod.replay.option.showChat=Hiển thị Chat
+
+sol_client.mod.replay.option.camera=Dạng Camera
+sol_client.mod.replay.option.camera.classic=Cổ điển
+sol_client.mod.replay.option.camera.vanilla_ish=Vanilla-ish
+
+sol_client.mod.replay.option.showPathPreview=Hiển thị đường di chuyển của máy quay
+
+sol_client.mod.replay.option.defaultInterpolator=Interpolator mặc định
+sol_client.mod.replay.interpolator.catmull=Catmull-Rom Spline
+sol_client.mod.replay.interpolator.cubic=Cubic Spline
+sol_client.mod.replay.interpolator.linear=Linear
+
+sol_client.mod.replay.option.showServerIPs=Hiển thị địa chỉ máy chủ
+
+sol_client.mod.quickplay.name=Quick Play
+sol_client.mod.quickplay.description=Tự động tham gia game mới bằng phím tắt trên bàn phím.
+
+sol_client.mod.quickplay.key=Quick Play
+sol_client.mod.quickplay.option.menuKey=Phím tắt
+
+sol_client.mod.hypixel_util.name=Hypixel Additions
+sol_client.mod.hypixel_util.description=Các tuỳ chỉnh cải thiện trải nghiệm khi chơi Hypixel.
+
+sol_client.mod.hypixel_util.option.visitHousingCommand=/visithousing
+sol_client.mod.hypixel_util.option.lobbySoundsVolume=Độ lớn âm thanh Lobby
+sol_client.mod.hypixel_util.option.housingMusicVolume=Độ lớn âm nhạc Housing
+sol_client.mod.hypixel_util.option.popupEvents=Popup Events
+sol_client.mod.hypixel_util.option.autogg=Tự động GG
+sol_client.mod.hypixel_util.option.autoggMessage=Tin nhắn GG
+sol_client.mod.hypixel_util.option.hidegg=Ẩn tin nhắn GG
+sol_client.mod.hypixel_util.option.autogl=Tự động GL
+sol_client.mod.hypixel_util.option.autoglMessage=Tin nhắn GL
+sol_client.mod.hypixel_util.option.hidegl=Ẩn tin nhắn GL
+sol_client.mod.hypixel_util.option.levelhead=Level Head
+
+sol_client.mod.tnt_timer.name=Bộ đếm thời gian TNT
+sol_client.mod.tnt_timer.description=Hiển thị thời gian nổ của TNT.
+
+sol_client.mod.motion_blur.name=Làm mờ chuyển động.
+sol_client.mod.motion_blur.description=Hiệu ứng mờ khi màn hình di chuyển.
+
+sol_client.mod.motion_blur.option.blur=Độ mờ
+
+sol_client.mod.menu_blur.name=Nền Menu Mờ
+sol_client.mod.menu_blur.description=Làm mờ nền khi đang mở túi đồ hay giao diện bất kì.
+
+sol_client.mod.menu_blur.option.blur=Độ mờ
+sol_client.mod.menu_blur.option.fadeTime=Thời gian bắt đầu làm mờ
+sol_client.mod.menu_blur.option.backgroundColour=Màu nền
+
+sol_client.mod.colour_saturation.name=Color Saturation
+sol_client.mod.colour_saturation.description=Tuỳ chỉnh độ bão hoà của màu sắc game.
+
+sol_client.mod.colour_saturation.option.saturation=Độ bão hoà
+
+sol_client.mod.chunk_animator.name=Chunk Animator
+sol_client.mod.chunk_animator.description=Hoạt hoạ khi tải thế giới.
+
+sol_client.mod.chunk_animator.option.duration=Tốc độ hoạt hoạ
+sol_client.mod.chunk_animator.option.animation=Hoạt hoạ
+
+sol_client.mod.1.7_visuals.name=Hiệu ứng 1.7
+sol_client.mod.1.7_visuals.description=Mang trở lại hiệu ứng phiên bản 1.7.
+
+sol_client.mod.1.7_visuals.option.useAndMine=Hiệu ứng đập khi dùng vật phẩm
+sol_client.mod.1.7_visuals.option.particles=Hạt hiệu ứng
+sol_client.mod.1.7_visuals.option.blocking=Chặn kiếm
+sol_client.mod.1.7_visuals.option.eatingAndDrinking=Ăn uống
+sol_client.mod.1.7_visuals.option.rod=Cần câu
+sol_client.mod.1.7_visuals.option.bow=Cung tên
+sol_client.mod.1.7_visuals.option.armourDamage=Hiệu ứng sát thương trên giáp
+sol_client.mod.1.7_visuals.option.sneaking=Làm mượt hiệu ứng đi rón rén
+
+sol_client.mod.item_physics.name=Hiệu ứng vật lí vật phẩm
+sol_client.mod.item_physics.description=Hoạt hoạ vật phẩm rơi trên mặt đất.
+
+sol_client.mod.item_physics.option.rotationSpeed=Tốc độ Quay
+
+sol_client.mod.particles.name=Hạt hiệu ứng
+sol_client.mod.particles.description=Tuỳ chỉnh hạt hiệu ứng sát thương.
+
+sol_client.mod.particles.option.multiplier=Số lượng hạt hiệu ứng
+sol_client.mod.particles.option.sharpness=Luôn hiện hiệu ứng Sắc bén
+sol_client.mod.particles.option.snow=Tuyết
+sol_client.mod.particles.option.slime=Chất nhầy
+sol_client.mod.particles.option.flames=Lửa
+
+sol_client.mod.time_changer.name=Thay đổi thời gian
+sol_client.mod.time_changer.description=Thay đổi thời gian thế giới.
+
+sol_client.mod.time_changer.option.time=Thời gian
+
+sol_client.mod.block_selection.name=Hiệu ứng chọn khối
+sol_client.mod.block_selection.description=Tuỳ chỉnh hiệu ứng khối đang chọn.
+
+sol_client.mod.block_selection.option.outline=Viền
+sol_client.mod.block_selection.option.outlineWidth=Độ lớn viền
+sol_client.mod.block_selection.option.outlineColour=Màu viền
+sol_client.mod.block_selection.option.fill=Nền
+sol_client.mod.block_selection.option.fillColour=Màu nền
+sol_client.mod.block_selection.option.depth=Ẩn các mặt bị che
+sol_client.mod.block_selection.option.persistent=Luôn hiển thị ở mọi chế độ
+
+sol_client.mod.hit_colour.name=Hit Color
+sol_client.mod.hit_colour.description=Đổi màu hit.
+
+sol_client.mod.hit_colour.option.colour=Màu
+
+sol_client.mod.hitbox.name=Hitbox
+sol_client.mod.hitbox.description=Tuỳ chỉnh, hiển thị hitbox người chơi.
+
+sol_client.mod.hitbox.option.toggleHitboxes=Hiển thị Hitbox
+sol_client.mod.hitbox.option.boundingBox=Hiển thị viền Hitbox
+sol_client.mod.hitbox.option.boundingBoxColour=Màu sắc viền Hitbox
+sol_client.mod.hitbox.option.eyeHeight=Hiển thị độ cao mắt
+sol_client.mod.hitbox.option.eyeHeightColour=Màu độ cao mắt
+sol_client.mod.hitbox.option.lookVector=Đường chỉ hướng nhìn của người chơi
+sol_client.mod.hitbox.option.lookVectorColour=Màu đường chỉ hướng hình
+sol_client.mod.hitbox.option.lineWidth=Độ lớn đường viền
+
+sol_client.mod.category.general.name=Chung
+sol_client.mod.category.hud.name=Giao diện
+sol_client.mod.category.utility.name=Khác
+sol_client.mod.category.visual.name=Hiển thị
+sol_client.mod.category.integration.name=Tương tác
+
+sol_client.mod.discord_integration.name=Tương tác với Discord
+sol_client.mod.discord_integration.description=Các tính năng liên kết với Discord.
+
+sol_client.mod.discord_integration.option.voiceChatHud=Hiển thị HUD Voice chat
+sol_client.mod.discord_integration.option.voiceChatHudScale=Kích thước HUD
+sol_client.mod.discord_integration.option.voiceChatHudAlignment=Căn lề HUD
+sol_client.mod.discord_integration.option.usernameColour=Màu tên tài khoản mặc định
+sol_client.mod.discord_integration.option.mutedColour=Màu tên tài khoản đã tắt Mic
+sol_client.mod.discord_integration.option.speakingColour=Màu tên tài khoản đang nói
+
+sol_client.mod.screen.title=Danh sách Mod
+sol_client.mod.screen.search=Gõ để tìm kiếm
+sol_client.mod.screen.apply_to_all=Áp dụng cho toàn bộ Mod khác
+
+sol_client.hud.edit=Chỉnh sửa HUD
+
+sol_client.packs.apply=Áp dụng
+sol_client.packs.open_folder=Thư mục tài nguyên
+sol_client.packs.folder=Thư mục (%d gói tài nguyên)
+
+sol_client.screenshot.view=Mở ảnh
+sol_client.screenshot.open_folder=Hiển thị trong Thư mục
diff --git a/game/src/main/resources/mixins.solclient.json b/game/src/main/resources/mixins.solclient.json
index 44f36a6e..e437ce1e 100644
--- a/game/src/main/resources/mixins.solclient.json
+++ b/game/src/main/resources/mixins.solclient.json
@@ -1,9 +1,9 @@
{
- "required": true,
- "package": "io.github.solclient.client.mixin",
- "compatibilityLevel": "JAVA_8",
- "refmap": "mixins.solclient.refmap.json",
- "mixins": [
+ "required": true,
+ "package": "io.github.solclient.client.mixin",
+ "compatibilityLevel": "JAVA_8",
+ "refmap": "mixins.solclient.refmap.json",
+ "mixins": [
"MixinEntity",
"MixinEntityLivingBase",
"MixinEntityPlayer",
@@ -85,9 +85,8 @@
"mod.MixinV1_7VisualsMod$MixinItemRenderer",
"mod.MixinV1_7VisualsMod$MixinLayerArmorBase",
"mod.MixinTNTTimerMod$MixinRenderTNTPrimed",
- "lwjgl.MixinWindowsDisplay"
- ],
- "injectors": {
- "defaultRequire": 1
- }
+ "lwjgl.MixinWindowsDisplay",
+ "lwjgl.MixinLinuxKeyboard",
+ "lwjgl.MixinLinuxKeycodes"
+ ]
}
diff --git a/launcher.js b/launcher.js
index 170c402a..3613e8a0 100644
--- a/launcher.js
+++ b/launcher.js
@@ -12,7 +12,7 @@ const Patcher = require("./patcher");
const { ipcRenderer, shell } = require("electron");
const crypto = require("crypto");
const url = require("url");
-const { AccountManager } = require("./auth");
+const { AccountManager } = require("./auth").AccountManager;
class Launcher {
@@ -21,22 +21,34 @@ class Launcher {
games = [];
async launch(callback, progress, server) {
- console.log("Downloading manifest...");
- progress("Loading manifest...");
- var manifest = await Manifest.getManifest();
- console.log("Downloading version manifest...");
- var version = await Manifest.getVersion(manifest, "1.8.9");
- var jars = [];
- var versionFolder = Version.getPath(version);
- var versionJar = Version.getJar(version);
- var nativesFolder = Version.getNatives(version);
- var optifineRelative = "net/optifine/optifine/1.8.9_HD_U_M5/optifine-1.8.9_HD_U_M5.jar";
- var optifine = Utils.librariesDirectory + "/" + optifineRelative;
- var secret = crypto.randomBytes(32).toString("hex");
- var alreadyRunning = this.games.length > 0;
+ const versionId = "1.8.9";
+ const jars = [];
+ const versionFolder = Version.getPath(versionId);
+ const versionJar = Version.getJar(versionId);
+ const versionJson = Version.getJson(versionId);
+
+ fs.mkdirSync(versionFolder, { recursive: true });
+
+ let version;
+ if(!fs.existsSync(versionJson)) {
+ console.log("Downloading version data...");
+ progress("Downloading version data...");
+ const manifest = await Manifest.getManifest();
+ version = await Manifest.getVersion(manifest, versionId);
+ fs.writeFileSync(versionJson, JSON.stringify(version), "UTF-8");
+ }
+ else {
+ version = JSON.parse(fs.readFileSync(versionJson, "UTF-8"))
+ }
+
+ const nativesFolder = Version.getNatives(versionId);
+ const optifineRelative = "net/optifine/optifine/1.8.9_HD_U_M5/optifine-1.8.9_HD_U_M5.jar";
+ const optifine = Utils.librariesDirectory + "/" + optifineRelative;
+ const secret = crypto.randomBytes(32).toString("hex");
+ const alreadyRunning = this.games.length > 0;
if(!alreadyRunning && fs.existsSync(nativesFolder)) {
- fs.rmdirSync(nativesFolder, { recursive: true });
+ fs.rmSync(nativesFolder, { recursive: true });
}
console.log("Downloading libraries...");
@@ -154,7 +166,7 @@ class Launcher {
}
});
- for(var library of version.libraries) {
+ for(let library of version.libraries) {
if(library.name == "org.apache.logging.log4j:log4j-api:2.0-beta9"
|| library.name == "org.apache.logging.log4j:log4j-core:2.0-beta9"
|| library.name == "com.google.code.gson:gson:2.2.4") {
@@ -171,12 +183,12 @@ class Launcher {
}
if(!alreadyRunning && library.natives != null) {
- var nativeName = library.natives[Utils.getOsName()];
+ let nativeName = library.natives[Utils.getOsName()];
if(nativeName != null) {
- var download = library.downloads.classifiers[nativeName];
+ let download = library.downloads.classifiers[nativeName];
if(download != null) {
await Library.download(download);
- var zip = fs.createReadStream(Library.getPath(download))
+ let zip = fs.createReadStream(Library.getPath(download))
.pipe(unzipper.Parse({ forceStream: true }));
for await(const entry of zip) {
@@ -186,12 +198,12 @@ class Launcher {
await entry.autodrain();
}
else {
- var destination = nativesFolder + "/" + fileName;
+ let destination = nativesFolder + "/" + fileName;
if(!fs.existsSync(path.dirname(destination))) {
fs.mkdirSync(path.dirname(destination), { recursive: true });
}
- await entry.pipe(fs.createWriteStream(nativesFolder + "/" + fileName));
+ await entry.pipe(fs.createWriteStream(nativesFolder + "/" + fileName));
}
}
}
@@ -202,12 +214,12 @@ class Launcher {
console.log("Downloading assets...");
progress("Downloading assets...");
- var assetIndex = await Version.getAssetIndex(version);
+ let assetIndex = await Version.getAssetIndex(version);
assetIndex.id = version.assetIndex.id;
AssetIndex.save(assetIndex);
- for(var object of Object.values(assetIndex.objects)) {
+ for(let object of Object.values(assetIndex.objects)) {
await AssetIndex.download(object);
}
@@ -217,94 +229,107 @@ class Launcher {
await Version.downloadJar(version);
console.log("Downloading JRE...");
- progress("Downloading Java...");
-
- var java;
-
- await new Promise((resolve) => {
- axios.get("https://api.adoptium.net/v3/assets/feature_releases/8/ga" +
- "?release_type=ga" +
- `&architecture=${os.arch()}` +
- "&heap_size=normal" +
- "&image_type=jre" +
- "&jvm_impl=hotspot" +
- `&os=${Utils.getJdkOsName()}` +
- "&page=0" +
- "&page_size=1" +
- "&project=jdk" +
- "&sort_method=DATE" +
- "&sort_order=DESC" +
- "&vendor=eclipse")
- .then(async(response) => {
- var jrePackage = response.data[0].binaries[0].package;
- var name = jrePackage.name;
- var jrePath = Utils.dataDirectory + "/jre/" + name;
- var dest = Utils.dataDirectory + "/jre/"
- + name.substring(0, name.indexOf("."));
- var doneFile = dest + "/.done";
- if(!fs.existsSync(dest + "/.done")) {
- await Utils.download(jrePackage.link,
- jrePath, jrePackage.size);
-
-
- if(!fs.existsSync(dest)) {
- fs.mkdirSync(dest, {recursive: true});
- }
+ progress("Downloading runtime...");
- if(name.endsWith(".tar.gz")) {
- await tar.x({
- file: jrePath,
- C: dest
- });
- }
- else if(name.endsWith(".zip")) {
- var zip = fs.createReadStream(jrePath)
- .pipe(unzipper.Parse({ forceStream: true }));
+ let jrePath = Config.data.jrePath;
+ let java;
- for await(const entry of zip) {
- const fileName = entry.path;
+ if(jrePath) {
+ java = path.join(jrePath, "bin/java")
+ }
- var destination = dest + "/" + fileName;
- if(!fs.existsSync(path.dirname(destination))) {
- fs.mkdirSync(path.dirname(destination), { recursive: true });
- }
+ if(!jrePath || !java || !fs.existsSync(java)) {
+ await new Promise((resolve) => {
+ axios.get("https://api.adoptium.net/v3/assets/feature_releases/8/ga" +
+ "?release_type=ga" +
+ `&architecture=${os.arch()}` +
+ "&heap_size=normal" +
+ "&image_type=jre" +
+ "&jvm_impl=hotspot" +
+ `&os=${Utils.getJdkOsName()}` +
+ "&page=0" +
+ "&page_size=1" +
+ "&project=jdk" +
+ "&sort_method=DATE" +
+ "&sort_order=DESC" +
+ "&vendor=eclipse")
+ .then(async(response) => {
+ const jreBinary = response.data[0].binaries[0];
+ const jrePackage = jreBinary.package;
+ const packageName = jrePackage.name;
+ const jrePath = Utils.dataDirectory + "/jre/" + packageName;
+ const dest = Utils.dataDirectory + "/jre/" + jrePackage.checksum;
+ const doneFile = dest + "/.done";
+
+ if(!fs.existsSync(dest + "/.done")) {
+ await Utils.download(jrePackage.link,
+ jrePath, jrePackage.size);
+
+
+ if(!fs.existsSync(dest)) {
+ fs.mkdirSync(dest, { recursive: true });
+ }
- await entry.pipe(fs.createWriteStream(destination));
+ if(packageName.endsWith(".tar.gz")) {
+ await tar.x({
+ file: jrePath,
+ C: dest
+ });
}
- }
+ else if(packageName.endsWith(".zip")) {
+ const zip = fs.createReadStream(jrePath)
+ .pipe(unzipper.Parse({ forceStream: true }));
- fs.closeSync(fs.openSync(doneFile, "w"));
+ for await(const entry of zip) {
+ const fileName = entry.path;
- fs.unlinkSync(jrePath);
- }
+ if(fileName.endsWith("/")) {
+ continue;
+ }
- java = dest + "/" + response.data[0].release_name
- + "-jre/";
- switch(Utils.getOsName()) {
- case "linux":
- java += "bin/java";
- break;
- case "windows":
- java += "bin/java.exe";
- break;
- case "osx":
- java += "Contents/Home/bin/java";
- break;
- }
+ const destination = dest + "/" + fileName;
+
+ if(!fs.existsSync(path.dirname(destination))) {
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
+ }
- resolve();
- });
- });
+ await entry.pipe(fs.createWriteStream(destination));
+ }
+ }
+
+ fs.closeSync(fs.openSync(doneFile, "w"));
+
+ fs.unlinkSync(jrePath);
+ }
+
+ java = dest + "/" + jreBinary.scm_ref.substring(0, jreBinary.scm_ref.lastIndexOf("_")) + "-jre";
+ switch(Utils.getOsName()) {
+ case "linux":
+ java = path.join(java, "bin/java");
+ break;
+ case "windows":
+ java = path.join(java, "bin/java.exe");
+ break;
+ case "osx":
+ java = path.join(java, "Contents/Home/bin/java");
+ break;
+ }
+
+ resolve();
+ });
+ });
+ }
console.log("Patching...");
progress("Patching...");
- var versionToAdd;
+ let versionToAdd;
+ let optifineVersion;
if(Config.data.optifine) {
- var optifinePatchedJar = versionFolder + "/" + version.id + "-patched-optifine.jar";
- var optifineSize = 2585014;
- var optifineVersion = "1.8.9_HD_U_M5";
+ let optifinePatchedJar = versionFolder + "/" + version.id + "-patched-optifine.jar";
+ let optifineSize = 2585014;
+ optifineVersion = "1.8.9_HD_U_M5";
await Library.download({
url: await Utils.getOptiFine(optifineVersion),
@@ -319,7 +344,7 @@ class Launcher {
versionToAdd = optifinePatchedJar;
}
else {
- var mappedJar = versionFolder + "/" + version.id + "-searge.jar";
+ const mappedJar = versionFolder + "/" + version.id + "-searge.jar";
if(!fs.existsSync(mappedJar)) {
await Patcher.patch(java, versionFolder, versionJar, mappedJar);
@@ -331,11 +356,11 @@ class Launcher {
console.log("Preparing Discord library...");
progress("Downloading Discord library...");
- var discordNativeLibrary;
+ let discordNativeLibrary;
- var discordVersion = "2.5.6";
- var discordPath = `com/discord/game-sdk/${discordVersion}/game-sdk-${discordVersion}.zip`;
- var discordFile = Utils.librariesDirectory + "/" + discordPath;
+ let discordVersion = "2.5.6";
+ let discordPath = `com/discord/game-sdk/${discordVersion}/game-sdk-${discordVersion}.zip`;
+ let discordFile = Utils.librariesDirectory + "/" + discordPath;
await Library.download({
url: "https://dl-game-sdk.discordapp.net/2.5.6/discord_game_sdk.zip",
@@ -343,10 +368,10 @@ class Launcher {
path: discordPath
});
- var sdkZip = fs.createReadStream(discordFile)
- .pipe(unzipper.Parse({ forceStream: true }));
+ let sdkZip = fs.createReadStream(discordFile)
+ .pipe(unzipper.Parse({ forceStream: true }));
- var suffix;
+ let suffix;
switch(Utils.getOsName()) {
case "windows":
@@ -360,9 +385,9 @@ class Launcher {
break;
}
- var discordLibraryName = "discord_game_sdk" + suffix;
+ let discordLibraryName = "discord_game_sdk" + suffix;
discordNativeLibrary = nativesFolder + "/" + discordLibraryName;
- var searchPath = "lib/x86_64/" + discordLibraryName;
+ let searchPath = "lib/x86_64/" + discordLibraryName;
for await(const entry of sdkZip) {
const fileName = entry.path;
@@ -371,19 +396,19 @@ class Launcher {
await entry.autodrain();
}
else {
- await entry.pipe(fs.createWriteStream(discordNativeLibrary));
+ await entry.pipe(fs.createWriteStream(discordNativeLibrary));
}
}
+ console.log("Starting...");
progress("Starting...");
- var args = [];
+ let args = [];
args.push("-Djava.library.path=" + nativesFolder);
args.push("-Dio.github.solclient.client.version=" + Utils.version);
args.push("-Dio.github.solclient.client.secret=" + secret);
- args.push("-Dmixin.target.mapid=searge");
- args.push("-Dlog4j2.formatMsgNoLookups=true"); // See https://hypixel.net/threads/understanding-the-recent-rce-exploit-for-minecraft-and-what-it-actually-means.4703643/. Thank you Draconish and danterus on Discord for informing me of this.
+ args.push("-Dlog4j2.formatMsgNoLookups=true");
args.push("-Xmx" + Config.data.maxMemory + "M");
@@ -395,18 +420,22 @@ class Launcher {
// Fix crashing on some non-English setups. Basically, Mixin is broken in the current version.
// This shouldn't (at least I hope it doesn't) interfere with anything, and you can still select your own language from the menu.
+ // Update: it can conflict with decimal formatting - 1.3 will always appear in that format rather than 1,3.
args.push("-Duser.language=en");
args.push("-Duser.country=US");
// Fix Log4j encoding.
args.push("-Dfile.encoding=UTF-8");
- var classpathSeparator = Utils.getOsName() == "windows" ? ";" : ":";
- var classpath = "";
+ // Add custom args.
+ args.push(...Config.getJvmArgs());
+
+ let classpathSeparator = Utils.getOsName() == "windows" ? ";" : ":";
+ let classpath = "";
args.push("-cp");
- for(var jar of jars) {
+ for(let jar of jars) {
classpath += jar;
classpath += classpathSeparator;
}
@@ -423,7 +452,7 @@ class Launcher {
args.push("--version");
args.push("Sol Client");
- var activeAccount = this.accountManager.activeAccount;
+ let activeAccount = this.accountManager.activeAccount;
args.push("--username");
args.push(activeAccount.username);
@@ -437,7 +466,7 @@ class Launcher {
}
args.push("--accessToken");
- args.push(activeAccount.accessToken);
+ args.push(await this.accountManager.realToken(activeAccount));
args.push("--versionType");
args.push("release");
@@ -452,7 +481,7 @@ class Launcher {
args.push("--assetIndex");
args.push(version.assetIndex.id);
- var gameDirectory = Config.getGameDirectory(Utils.gameDirectory);
+ let gameDirectory = Config.getGameDirectory(Utils.gameDirectory);
args.push("--gameDir");
args.push(gameDirectory);
@@ -460,45 +489,23 @@ class Launcher {
args.push("--tweakClass");
args.push("io.github.solclient.client.tweak.Tweaker");
- var process = childProcess.spawn(java, args, { cwd: gameDirectory });
+ let process = childProcess.spawn(java, args, { cwd: gameDirectory });
this.games.push(process);
let fullOutput = "";
process.stdout.on("data", (data) => {
- var dataString = data.toString("UTF-8");
+ let dataString = data.toString("UTF-8");
fullOutput += dataString;
if(dataString.endsWith("\n")) {
dataString = dataString.substring(0, dataString.length - 1);
}
- if(dataString.indexOf("message ") == 0) {
- var splitDataString = dataString.split(" ");
- if(splitDataString[1] === secret) {
- if(splitDataString[2] == "openUrl") {
- var openUrl = splitDataString[3];
-
- if(Utils.getOsName() == "windows") {
- openUrl = openUrl.substring(0, openUrl.length - 1);
- }
-
- if(openUrl.endsWith("§scshowinfolder§")) {
- openUrl = openUrl.substring(0, openUrl.length - 16);
- shell.showItemInFolder(url.fileURLToPath(openUrl));
- }
- else {
- shell.openExternal(openUrl);
- }
- }
- }
- }
- else {
- console.log("[Game/STDOUT] " + dataString);
- }
+ console.log("[Game/STDOUT] " + dataString);
});
process.stderr.on("data", (data) => {
- var dataString = data.toString("UTF-8");
+ let dataString = data.toString("UTF-8");
fullOutput += dataString;
console.error("[Game/STDERR] " + dataString);
});
@@ -506,8 +513,11 @@ class Launcher {
process.on("exit", (code) => {
if(code != 0) {
console.error("Game crashed with exit code " + code);
+
+ let optifineName;
+
if(optifineVersion) {
- var optifineName = "OptiFine " + optifineVersion.replace(/_/g, " ");
+ optifineName = "OptiFine " + optifineVersion.replace(/_/g, " ");
}
ipcRenderer.send("crash", fullOutput, Config.getGameDirectory(Utils.gameDirectory) + "/logs/latest.log", optifineName);
@@ -541,7 +551,7 @@ class Manifest {
return;
}
- var body = "";
+ let body = "";
response.on("data", (data) => {
body += data;
});
@@ -554,7 +564,7 @@ class Manifest {
static getVersion(manifest, id) {
return new Promise((resolve, reject) => {
- for(var version of manifest.versions) {
+ for(let version of manifest.versions) {
if(version.id == id) {
https.get(version.url, (response, error) => {
if(error) {
@@ -562,7 +572,7 @@ class Manifest {
return;
}
- var body = "";
+ let body = "";
response.on("data", (data) => {
body += data;
});
@@ -586,7 +596,7 @@ class Version {
if(response.code == 404) {
resolve(null);
}
- var body = "";
+ let body = "";
response.on("data", (data) => {
body += data;
});
@@ -598,19 +608,23 @@ class Version {
}
static getPath(version) {
- return Utils.versionsDirectory + "/" + version.id;
+ return Utils.versionsDirectory + "/" + version;
}
static getJar(version) {
- return Version.getPath(version) + "/" + version.id + ".jar";
+ return Version.getPath(version) + "/" + version + ".jar";
+ }
+
+ static getJson(version) {
+ return Version.getPath(version) + "/" + version + ".json";
}
static getNatives(version) {
- return Version.getPath(version) + "/" + version.id + "-natives";
+ return Version.getPath(version) + "/" + version + "-natives";
}
static downloadJar(version) {
- return Utils.download(version.downloads.client.url, Version.getJar(version), version.downloads.client.size);
+ return Utils.download(version.downloads.client.url, Version.getJar(version.id), version.downloads.client.size);
}
}
@@ -626,8 +640,8 @@ class Library {
return true;
}
- var result = false;
- for(var rule of rules) {
+ let result = false;
+ for(let rule of rules) {
if(rule.os != null) {
if(rule.os.name == Utils.getOsName()) {
return rule.action == "allow";
@@ -661,7 +675,7 @@ class AssetIndex {
}
static save(index) {
- var indexPath = AssetIndex.getIndexPath(index);
+ let indexPath = AssetIndex.getIndexPath(index);
if(!fs.existsSync(path.dirname(indexPath))) {
fs.mkdirSync(path.dirname(indexPath), { recursive: true });
diff --git a/main.js b/main.js
index 0540e595..8a827b12 100644
--- a/main.js
+++ b/main.js
@@ -26,11 +26,11 @@ async function run() {
const msmc = require("msmc");
const hastebin = require("hastebin");
- var window;
- var canQuit = false;
+ let window;
+ let canQuit = false;
function createWindow() {
- var options = {
+ let options = {
width: 800,
height: 650,
icon: __dirname + "/assets/icon.png",
@@ -39,7 +39,8 @@ async function run() {
},
title: "Sol Client " + Utils.version,
show: false,
- backgroundColor: "#1e1e1e"
+ backgroundColor: "#1e1e1e",
+ darkTheme: true
};
if(Utils.getOsName() == "osx") {
@@ -51,6 +52,10 @@ async function run() {
window.loadFile("app.html");
window.setMenu(null);
+ if(process.env.DEVTOOLS) {
+ window.webContents.openDevTools();
+ }
+
window.on("close", (event) => {
if(!canQuit) {
event.preventDefault();
@@ -60,23 +65,30 @@ async function run() {
window.once("ready-to-show", () => window.show());
- ipcMain.on("directory", async(event) => {
- var result = await dialog.showOpenDialog(window,
+ ipcMain.on("directory", async(event, title, id) => {
+ let result = await dialog.showOpenDialog(window,
{
- title: "Select Minecraft Folder",
+ title: title,
properties: ["openDirectory" ]
}
);
- var file = result.filePaths[0];
+ let file = result.filePaths[0];
if(!result.canceled && file) {
- event.sender.send("directory", file);
+ event.sender.send("directory", file, id);
}
});
+ ipcMain.on("jreError", async(event) => {
+ dialog.showMessageBoxSync(window, {
+ title: "Invalid Directory",
+ message: "JRE must include bin folder."
+ });
+ });
+
ipcMain.on("skinFile", async(event) => {
- var result = await dialog.showOpenDialog(window,
+ let result = await dialog.showOpenDialog(window,
{
title: "Select Skin File",
filters: [
@@ -92,7 +104,7 @@ async function run() {
}
);
- var file = result.filePaths[0];
+ let file = result.filePaths[0];
if(!result.canceled && file) {
event.sender.send("skinFile", file);
@@ -102,13 +114,14 @@ async function run() {
ipcMain.on("msa", async(event) => {
msmc.fastLaunch("electron", () => {})
- .then((result) => {
- event.sender.send("msa", JSON.stringify(result));
- });
+ .then(async(result) => {
+ await window.webContents.session.clearStorageData();
+ event.sender.send("msa", JSON.stringify(result));
+ });
});
ipcMain.on("crash", async(_event, report, file, optifine) => {
- var option = dialog.showMessageBoxSync(window, {
+ let option = dialog.showMessageBoxSync(window, {
title: "Game Crashed",
message: `The game has crashed.
You may submit a report on GitHub, so it can be fixed.
@@ -122,8 +135,6 @@ If you have private messages, try reproducing this issue again.`,
]
});
- // Indentation matters.
-
if(option == 1) {
shell.openPath(file);
}
@@ -131,40 +142,39 @@ If you have private messages, try reproducing this issue again.`,
return;
}
- var crashReportText = "Add any applicable crash reports, making sure not to include any personal information. It is most important that you do not include the session id.";
+ let crashReportText = "Add any applicable crash reports, making sure not to include any personal information. It is most important that you do not include the session id.";
if(report) {
report = report.replace(/\[.*\] \[.*\]: \(Session ID is .{3,}\)/gm, "");
hasteUrl = await hastebin.createPaste(report, {
raw: true,
- contentType: "text/plain",
- server: "https://www.toptal.com/developers/hastebin/"
+ contentType: "text/plain"
});
- hasteUrl = "https://www.toptal.com/developers/hastebin/" + hasteUrl.substring(hasteUrl.lastIndexOf('/') + 1) + ".txt";
crashReportText = `
[Game Log on Hastebin](${hasteUrl})`
}
- var running = `Running Sol Client v${Utils.version}`;
+ let running = `Running Sol Client v${Utils.version}`;
if(optifine) {
running += " with " + optifine;
}
+
+ running += " on " + Utils.getNiceOsName();
running += ".";
- var url = new URL("https://github.com/TheKodeToad/Sol-Client/issues/new/")
- url.searchParams.set("body", `## Description
+ let url = new URL("https://github.com/TheKodeToad/Sol-Client/issues/new/")
+ url.searchParams.set("body", `## Description (please fill in)
A description of the problem that is occurring.
## Steps to Reproduce
1. What did you do...
2. ...to crash the game?
-## Client Version
+## Details
${running}
## Logs/Crash Report
${crashReportText}
`);
- url.searchParams.set("title", "Short Description")
url.searchParams.set("labels", "bug");
shell.openExternal(url.toString());
});
diff --git a/package-lock.json b/package-lock.json
index 60dae114..b5ddef96 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "Sol Client",
- "version": "1.8.2",
+ "version": "1.8.9",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "Sol Client",
- "version": "1.8.2",
+ "version": "1.8.9",
"license": "GPL-3.0+",
"dependencies": {
"archiver": "^5.3.0",
@@ -14,10 +14,12 @@
"electron-is-dev": "^2.0.0",
"electron-squirrel-startup": "^1.0.0",
"hastebin": "^0.2.1",
- "msmc": "^3.1.1",
+ "keytar": "^7.9.0",
+ "msmc": "^3.1.3",
"nbt": "^0.8.1",
"tar": "^6.1.11",
- "unzipper": "^0.10.11"
+ "unzipper": "^0.10.11",
+ "xss": "^1.0.14"
},
"devDependencies": {
"@electron-forge/cli": "^6.0.0-beta.61",
@@ -2838,6 +2840,11 @@
"node": ">=12.10"
}
},
+ "node_modules/cssfilter": {
+ "version": "0.0.10",
+ "resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz",
+ "integrity": "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw=="
+ },
"node_modules/cuint": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/cuint/-/cuint-0.2.2.tgz",
@@ -2896,6 +2903,14 @@
"node": ">=4"
}
},
+ "node_modules/deep-extend": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
+ "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
"node_modules/defaults": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz",
@@ -4351,6 +4366,14 @@
"node": ">=0.8"
}
},
+ "node_modules/expand-template": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
+ "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/expand-tilde": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz",
@@ -4874,6 +4897,11 @@
"assert-plus": "^1.0.0"
}
},
+ "node_modules/github-from-package": {
+ "version": "0.0.0",
+ "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
+ "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="
+ },
"node_modules/glob": {
"version": "7.1.7",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz",
@@ -5353,8 +5381,7 @@
"node_modules/ini": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
- "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
- "dev": true
+ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="
},
"node_modules/inquirer": {
"version": "8.1.5",
@@ -5724,6 +5751,21 @@
"node": ">=8"
}
},
+ "node_modules/keytar": {
+ "version": "7.9.0",
+ "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz",
+ "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==",
+ "hasInstallScript": true,
+ "dependencies": {
+ "node-addon-api": "^4.3.0",
+ "prebuild-install": "^7.0.1"
+ }
+ },
+ "node_modules/keytar/node_modules/node-addon-api": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz",
+ "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ=="
+ },
"node_modules/keyv": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz",
@@ -5887,7 +5929,6 @@
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
- "dev": true,
"dependencies": {
"yallist": "^4.0.0"
},
@@ -6195,6 +6236,11 @@
"mkdirp": "bin/cmd.js"
}
},
+ "node_modules/mkdirp-classic": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
+ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="
+ },
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@@ -6202,9 +6248,9 @@
"dev": true
},
"node_modules/msmc": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/msmc/-/msmc-3.1.1.tgz",
- "integrity": "sha512-0M1s8r1Xn1MQ4kDOlGpTh7pkKLRDmhp5baIZpque71H/jsIJRgZ9CY6MSrj6ltEll2cNkUWRa1vUG4vATSwNpw==",
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/msmc/-/msmc-3.1.3.tgz",
+ "integrity": "sha512-qYTE8XciN/TgxwrCfxH3tJriK6qI6Vn0KLTtAXSKeKX9xa4jkD3Mo9cyhisDReY2LQIUvBa/nEynz1qY9WWwew==",
"funding": [
{
"type": "paypal",
@@ -6243,6 +6289,11 @@
"dev": true,
"optional": true
},
+ "node_modules/napi-build-utils": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz",
+ "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg=="
+ },
"node_modules/nbt": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/nbt/-/nbt-0.8.1.tgz",
@@ -6267,7 +6318,6 @@
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.8.0.tgz",
"integrity": "sha512-tzua9qWWi7iW4I42vUPKM+SfaF0vQSLAm4yO5J83mSwB7GeoWrDKC/K+8YCnYNwqP5duwazbw2X9l4m8SC2cUw==",
- "dev": true,
"dependencies": {
"semver": "^7.3.5"
},
@@ -6279,7 +6329,6 @@
"version": "7.3.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
- "dev": true,
"dependencies": {
"lru-cache": "^6.0.0"
},
@@ -6996,6 +7045,39 @@
"node": ">=4.0"
}
},
+ "node_modules/prebuild-install": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz",
+ "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==",
+ "dependencies": {
+ "detect-libc": "^2.0.0",
+ "expand-template": "^2.0.3",
+ "github-from-package": "0.0.0",
+ "minimist": "^1.2.3",
+ "mkdirp-classic": "^0.5.3",
+ "napi-build-utils": "^1.0.1",
+ "node-abi": "^3.3.0",
+ "pump": "^3.0.0",
+ "rc": "^1.2.7",
+ "simple-get": "^4.0.0",
+ "tar-fs": "^2.0.0",
+ "tunnel-agent": "^0.6.0"
+ },
+ "bin": {
+ "prebuild-install": "bin.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/prebuild-install/node_modules/detect-libc": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz",
+ "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/prepend-http": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz",
@@ -7166,6 +7248,20 @@
"murmur-32": "^0.1.0 || ^0.2.0"
}
},
+ "node_modules/rc": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
+ "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
+ "dependencies": {
+ "deep-extend": "^0.6.0",
+ "ini": "~1.3.0",
+ "minimist": "^1.2.0",
+ "strip-json-comments": "~2.0.1"
+ },
+ "bin": {
+ "rc": "cli.js"
+ }
+ },
"node_modules/rcedit": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/rcedit/-/rcedit-3.0.1.tgz",
@@ -7647,6 +7743,74 @@
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
"dev": true
},
+ "node_modules/simple-concat": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
+ "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/simple-get": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
+ "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "dependencies": {
+ "decompress-response": "^6.0.0",
+ "once": "^1.3.1",
+ "simple-concat": "^1.0.0"
+ }
+ },
+ "node_modules/simple-get/node_modules/decompress-response": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
+ "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
+ "dependencies": {
+ "mimic-response": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/simple-get/node_modules/mimic-response": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
+ "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/single-line-log": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/single-line-log/-/single-line-log-1.1.2.tgz",
@@ -7910,6 +8074,14 @@
"node": ">=0.10.0"
}
},
+ "node_modules/strip-json-comments": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+ "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/strip-outer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz",
@@ -7977,6 +8149,22 @@
"node": ">= 10"
}
},
+ "node_modules/tar-fs": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz",
+ "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==",
+ "dependencies": {
+ "chownr": "^1.1.1",
+ "mkdirp-classic": "^0.5.2",
+ "pump": "^3.0.0",
+ "tar-stream": "^2.1.4"
+ }
+ },
+ "node_modules/tar-fs/node_modules/chownr": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
+ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="
+ },
"node_modules/tar-stream": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
@@ -8305,7 +8493,6 @@
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
"integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
- "dev": true,
"dependencies": {
"safe-buffer": "^5.0.1"
},
@@ -8565,6 +8752,26 @@
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
+ "node_modules/xss": {
+ "version": "1.0.14",
+ "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.14.tgz",
+ "integrity": "sha512-og7TEJhXvn1a7kzZGQ7ETjdQVS2UfZyTlsEdDOqvQF7GoxNfY+0YLCzBy1kPdsDDx4QuNAonQPddpsn6Xl/7sw==",
+ "dependencies": {
+ "commander": "^2.20.3",
+ "cssfilter": "0.0.10"
+ },
+ "bin": {
+ "xss": "bin/xss"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ }
+ },
+ "node_modules/xss/node_modules/commander": {
+ "version": "2.20.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
+ },
"node_modules/xtend": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz",
@@ -10936,6 +11143,11 @@
"integrity": "sha512-MEzGfZo0rqE10O/B+AEcCSJLZsrWuRUvmqJTqHNqBtALhaJc3E3ixLGLJNTRzEA2K34wbmOHC4fwYs9sVsdcCA==",
"dev": true
},
+ "cssfilter": {
+ "version": "0.0.10",
+ "resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz",
+ "integrity": "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw=="
+ },
"cuint": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/cuint/-/cuint-0.2.2.tgz",
@@ -10977,6 +11189,11 @@
"mimic-response": "^1.0.0"
}
},
+ "deep-extend": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
+ "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="
+ },
"defaults": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz",
@@ -12104,6 +12321,11 @@
"resolved": "https://registry.npmjs.org/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz",
"integrity": "sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw=="
},
+ "expand-template": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
+ "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="
+ },
"expand-tilde": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz",
@@ -12537,6 +12759,11 @@
"assert-plus": "^1.0.0"
}
},
+ "github-from-package": {
+ "version": "0.0.0",
+ "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
+ "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="
+ },
"glob": {
"version": "7.1.7",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz",
@@ -12901,8 +13128,7 @@
"ini": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
- "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
- "dev": true
+ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="
},
"inquirer": {
"version": "8.1.5",
@@ -13190,6 +13416,22 @@
"integrity": "sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ==",
"dev": true
},
+ "keytar": {
+ "version": "7.9.0",
+ "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz",
+ "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==",
+ "requires": {
+ "node-addon-api": "^4.3.0",
+ "prebuild-install": "^7.0.1"
+ },
+ "dependencies": {
+ "node-addon-api": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz",
+ "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ=="
+ }
+ }
+ },
"keyv": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz",
@@ -13331,7 +13573,6 @@
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
- "dev": true,
"requires": {
"yallist": "^4.0.0"
}
@@ -13559,6 +13800,11 @@
"minimist": "^1.2.5"
}
},
+ "mkdirp-classic": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
+ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="
+ },
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@@ -13566,9 +13812,9 @@
"dev": true
},
"msmc": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/msmc/-/msmc-3.1.1.tgz",
- "integrity": "sha512-0M1s8r1Xn1MQ4kDOlGpTh7pkKLRDmhp5baIZpque71H/jsIJRgZ9CY6MSrj6ltEll2cNkUWRa1vUG4vATSwNpw==",
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/msmc/-/msmc-3.1.3.tgz",
+ "integrity": "sha512-qYTE8XciN/TgxwrCfxH3tJriK6qI6Vn0KLTtAXSKeKX9xa4jkD3Mo9cyhisDReY2LQIUvBa/nEynz1qY9WWwew==",
"requires": {
"node-fetch": "2.x"
}
@@ -13598,6 +13844,11 @@
"dev": true,
"optional": true
},
+ "napi-build-utils": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz",
+ "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg=="
+ },
"nbt": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/nbt/-/nbt-0.8.1.tgz",
@@ -13619,7 +13870,6 @@
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.8.0.tgz",
"integrity": "sha512-tzua9qWWi7iW4I42vUPKM+SfaF0vQSLAm4yO5J83mSwB7GeoWrDKC/K+8YCnYNwqP5duwazbw2X9l4m8SC2cUw==",
- "dev": true,
"requires": {
"semver": "^7.3.5"
},
@@ -13628,7 +13878,6 @@
"version": "7.3.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
- "dev": true,
"requires": {
"lru-cache": "^6.0.0"
}
@@ -14154,6 +14403,32 @@
}
}
},
+ "prebuild-install": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz",
+ "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==",
+ "requires": {
+ "detect-libc": "^2.0.0",
+ "expand-template": "^2.0.3",
+ "github-from-package": "0.0.0",
+ "minimist": "^1.2.3",
+ "mkdirp-classic": "^0.5.3",
+ "napi-build-utils": "^1.0.1",
+ "node-abi": "^3.3.0",
+ "pump": "^3.0.0",
+ "rc": "^1.2.7",
+ "simple-get": "^4.0.0",
+ "tar-fs": "^2.0.0",
+ "tunnel-agent": "^0.6.0"
+ },
+ "dependencies": {
+ "detect-libc": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz",
+ "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w=="
+ }
+ }
+ },
"prepend-http": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz",
@@ -14274,6 +14549,17 @@
"murmur-32": "^0.1.0 || ^0.2.0"
}
},
+ "rc": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
+ "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
+ "requires": {
+ "deep-extend": "^0.6.0",
+ "ini": "~1.3.0",
+ "minimist": "^1.2.0",
+ "strip-json-comments": "~2.0.1"
+ }
+ },
"rcedit": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/rcedit/-/rcedit-3.0.1.tgz",
@@ -14655,6 +14941,36 @@
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
"dev": true
},
+ "simple-concat": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
+ "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="
+ },
+ "simple-get": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
+ "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
+ "requires": {
+ "decompress-response": "^6.0.0",
+ "once": "^1.3.1",
+ "simple-concat": "^1.0.0"
+ },
+ "dependencies": {
+ "decompress-response": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
+ "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
+ "requires": {
+ "mimic-response": "^3.1.0"
+ }
+ },
+ "mimic-response": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
+ "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="
+ }
+ }
+ },
"single-line-log": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/single-line-log/-/single-line-log-1.1.2.tgz",
@@ -14868,6 +15184,11 @@
"integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=",
"dev": true
},
+ "strip-json-comments": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+ "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="
+ },
"strip-outer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz",
@@ -14929,6 +15250,24 @@
}
}
},
+ "tar-fs": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz",
+ "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==",
+ "requires": {
+ "chownr": "^1.1.1",
+ "mkdirp-classic": "^0.5.2",
+ "pump": "^3.0.0",
+ "tar-stream": "^2.1.4"
+ },
+ "dependencies": {
+ "chownr": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
+ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="
+ }
+ }
+ },
"tar-stream": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
@@ -15202,7 +15541,6 @@
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
"integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
- "dev": true,
"requires": {
"safe-buffer": "^5.0.1"
}
@@ -15416,6 +15754,22 @@
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
+ "xss": {
+ "version": "1.0.14",
+ "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.14.tgz",
+ "integrity": "sha512-og7TEJhXvn1a7kzZGQ7ETjdQVS2UfZyTlsEdDOqvQF7GoxNfY+0YLCzBy1kPdsDDx4QuNAonQPddpsn6Xl/7sw==",
+ "requires": {
+ "commander": "^2.20.3",
+ "cssfilter": "0.0.10"
+ },
+ "dependencies": {
+ "commander": {
+ "version": "2.20.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
+ }
+ }
+ },
"xtend": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz",
diff --git a/package.json b/package.json
index 7546d922..94581ef0 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "Sol Client",
- "version": "1.8.2",
+ "version": "1.8.9",
"description": "Simple and easy to use Minecraft client.",
"main": "main.js",
"scripts": {
@@ -18,10 +18,12 @@
"electron-is-dev": "^2.0.0",
"electron-squirrel-startup": "^1.0.0",
"hastebin": "^0.2.1",
- "msmc": "^3.1.1",
+ "keytar": "^7.9.0",
+ "msmc": "^3.1.3",
"nbt": "^0.8.1",
"tar": "^6.1.11",
- "unzipper": "^0.10.11"
+ "unzipper": "^0.10.11",
+ "xss": "^1.0.14"
},
"devDependencies": {
"@electron-forge/cli": "^6.0.0-beta.61",
diff --git a/patcher.js b/patcher.js
index da950d71..a0a536dc 100644
--- a/patcher.js
+++ b/patcher.js
@@ -7,20 +7,20 @@ const archiver = require("archiver");
class Patcher {
static async patch(java, versionFolder, versionJar, outputFile, optiFine) {
- var tempFolder = versionFolder + "/patch/";
+ let tempFolder = versionFolder + "/patch/";
if(fs.existsSync(tempFolder)) {
- fs.rmdirSync(tempFolder, { recursive: true });
+ fs.rmSync(tempFolder, { recursive: true });
}
fs.mkdirSync(tempFolder);
- var inputJar = versionJar;
+ let inputJar = versionJar;
if(optiFine) {
- var optiFineMod = tempFolder + "/optifine-mod.jar";
+ let optiFineMod = tempFolder + "/optifine-mod.jar";
inputJar = tempFolder + "/optifine-patched.jar";
await new Promise((resolve) => {
- var process = childProcess.spawn(java, [
+ let process = childProcess.spawn(java, [
"-cp",
optiFine,
"optifine.Patcher",
@@ -36,12 +36,12 @@ class Patcher {
});
await new Promise(async(resolve) => {
- var optiFinePatchedArchiver = archiver("zip");
+ let optiFinePatchedArchiver = archiver("zip");
optiFinePatchedArchiver.pipe(fs.createWriteStream(inputJar));
async function insert(jar) {
- var zip = fs.createReadStream(jar).pipe(
+ let zip = fs.createReadStream(jar).pipe(
unzipper.Parse({ forceStream: true }));
for await(const entry of zip) {
@@ -67,16 +67,16 @@ class Patcher {
});
}
- var mapped = tempFolder + "/mapped.jar";
- var specialSource = tempFolder + "/SpecialSource.jar";
- var joinedSrg = tempFolder + "/joined.srg";
- var mcpZip = tempFolder + "/mcp.zip";
+ let mapped = tempFolder + "/mapped.jar";
+ let specialSource = tempFolder + "/SpecialSource.jar";
+ let joinedSrg = tempFolder + "/joined.srg";
+ let mcpZip = tempFolder + "/mcp.zip";
await Utils.download("https://repo.maven.apache.org/maven2/net/md-5/SpecialSource/1.7.4/SpecialSource-1.7.4-shaded.jar", specialSource, 1526537);
await Utils.download("https://maven.minecraftforge.net/de/oceanlabs/mcp/mcp/1.8.9/mcp-1.8.9-srg.zip", mcpZip, 471509);
- var zip = fs.createReadStream(mcpZip).pipe(
+ let zip = fs.createReadStream(mcpZip).pipe(
unzipper.Parse({ forceStream: true }));
for await(const entry of zip) {
@@ -91,7 +91,7 @@ class Patcher {
}
await new Promise((resolve) => {
- var process = childProcess.spawn(java, [
+ let process = childProcess.spawn(java, [
"-jar",
specialSource,
"--in-jar",
@@ -109,7 +109,7 @@ class Patcher {
});
fs.renameSync(mapped, outputFile);
- fs.rmdirSync(tempFolder, { recursive: true });
+ fs.rmSync(tempFolder, { recursive: true });
}
}
diff --git a/style.css b/style.css
index 069307c3..00eb9a15 100644
--- a/style.css
+++ b/style.css
@@ -234,7 +234,7 @@
background: rgb(80, 80, 80);
}
-.login, .mojang-login {
+.login {
position: relative;
}
@@ -244,6 +244,17 @@
font-size: 2em;
}
+.microsoft-login-button {
+ margin-top: 50px;
+}
+
+.login-note {
+ position: fixed;
+ bottom: 30px;
+ width: 100%;
+ text-align: center;
+}
+
.login-box {
margin-left: auto;
margin-right: auto;
@@ -260,6 +271,12 @@
height: 30px;
margin-bottom: 30px;
padding: 5px;
+ background: black;
+}
+
+.settings .input {
+ padding: 5px;
+ background: rgb(30, 30, 30);
}
.label {
@@ -643,7 +660,7 @@ input[type="range"]:hover::-webkit-slider-thumb {
top: 50px;
}
-.news * {
+.news * {
max-width: 600px;
}
diff --git a/updater-dom.js b/updater-dom.js
index a74b562c..8012308b 100644
--- a/updater-dom.js
+++ b/updater-dom.js
@@ -1,7 +1,7 @@
const { ipcRenderer } = require("electron");
-var domLoaded = false;
-var deferredProgress;
+let domLoaded = false;
+let deferredProgress;
ipcRenderer.on("progress", (event, progress) => {
if(!domLoaded) {
diff --git a/updater.js b/updater.js
index 02803e74..96e953be 100644
--- a/updater.js
+++ b/updater.js
@@ -11,10 +11,10 @@ class Updater {
static update() {
return new Promise(async(resolve) => {
try {
- var fileExtension;
- var destFile;
- var currentVersion = require("./package.json").version;
- var appimage = process.env.APPIMAGE;
+ let fileExtension;
+ let destFile;
+ let currentVersion = require("./package.json").version;
+ let appimage = process.env.APPIMAGE;
if(Utils.getOsName() == "windows") {
fileExtension = ".exe";
@@ -43,7 +43,7 @@ class Updater {
console.log("Checking for update...");
- var latestRelease = (await axios.get("https://api.github.com/repos/TheKodeToad/Sol-Client/releases/latest")).data;
+ let latestRelease = (await axios.get("https://api.github.com/repos/TheKodeToad/Sol-Client/releases/latest")).data;
if(latestRelease.name == currentVersion) {
console.log("No updates found");
@@ -51,8 +51,8 @@ class Updater {
return;
}
- var selectedAsset;
- for(var asset of latestRelease.assets) {
+ let selectedAsset;
+ for(let asset of latestRelease.assets) {
if(asset.name.endsWith(fileExtension)) {
selectedAsset = asset;
}
@@ -68,7 +68,7 @@ class Updater {
await app.whenReady();
app.on("window-all-closed", (event) => event.preventDefault());
- var window = new BrowserWindow({
+ let window = new BrowserWindow({
width: 600,
height: 210,
icon: __dirname + "/assets/icon.png",
@@ -85,7 +85,7 @@ class Updater {
window.setMenu(null);
window.show();
- var wasClosed = false;
+ let wasClosed = false;
window.on("close", () => {
if(!wasClosed) {
@@ -107,13 +107,13 @@ class Updater {
await sleep(1000);
- var command = destFile;
+ let command = destFile;
if(Utils.getOsName() == "linux") {
fs.renameSync(destFile, appimage);
fs.chmodSync(appimage, 0o755);
if(path.basename(appimage).includes(currentVersion)) {
- var newName = path.join(path.dirname(appimage),
+ let newName = path.join(path.dirname(appimage),
path.basename(appimage).replace(currentVersion, latestRelease.name));
fs.renameSync(appimage, newName);
appimage = newName;
diff --git a/utils.js b/utils.js
index 708a15fb..af9cbeda 100644
--- a/utils.js
+++ b/utils.js
@@ -24,19 +24,15 @@ class Utils {
static init() {
Utils.dataDirectory = os.homedir();
- Utils.legacyDirectory = os.homedir();
switch(Utils.getOsName()) {
case "linux":
Utils.dataDirectory += "/.config/Sol Client";
- Utils.legacyDirectory += "/.config/parrotclient";
break;
case "osx":
Utils.dataDirectory += "/Library/Application Support/Sol Client";
- Utils.legacyDirectory += "/Library/Application Support/parrotclient";
break;
case "windows":
Utils.dataDirectory += "/AppData/Roaming/Sol Client";
- Utils.legacyDirectory += "/AppData/Roaming/parrotclient";
break;
}
@@ -53,15 +49,6 @@ class Utils {
break;
}
- try {
- if(fs.existsSync(Utils.legacyDirectory) && !fs.existsSync(Utils.dataDirectory)) {
- fs.renameSync(Utils.legacyDirectory, Utils.dataDirectory);
- fs.unlinkSync(Utils.dataDirectory + "/account.json");
- }
- }
- catch(error) {
- }
-
Utils.librariesDirectory = Utils.minecraftDirectory + "/libraries";
Utils.versionsDirectory = Utils.dataDirectory + "/versions";
Utils.assetsDirectory = Utils.minecraftDirectory + "/assets";
@@ -69,10 +56,10 @@ class Utils {
Utils.assetIndexesDirectory = Utils.assetsDirectory + "/indexes";
Utils.accountsFile = Utils.dataDirectory + "/accounts.json";
- var accountFile = Utils.dataDirectory + "/account.json";
+ let accountFile = Utils.dataDirectory + "/account.json";
if(fs.existsSync(accountFile) && !fs.existsSync(Utils.accountsFile)) {
- var oldAccount = JSON.parse(fs.readFileSync(accountFile, "UTF-8"));
- var converted = { accounts: [oldAccount], activeAccount: 0 }
+ let oldAccount = JSON.parse(fs.readFileSync(accountFile, "UTF-8"));
+ let converted = { accounts: [oldAccount], activeAccount: 0 }
fs.writeFileSync(Utils.accountsFile, JSON.stringify(converted));
fs.rmSync(accountFile);
}
@@ -106,7 +93,7 @@ class Utils {
return;
}
- var length;
+ let length;
if(response.headers["content-length"]) {
length = parseInt(response.headers["content-length"]);
}
@@ -114,7 +101,7 @@ class Utils {
length = 0;
}
- var receivedBytes = 0;
+ let receivedBytes = 0;
if(response.code > 400) {
reject(new Error("Server responded with error " + response.code));
@@ -122,12 +109,12 @@ class Utils {
}
if(response.headers.location) {
- var result = await Utils.download(response.headers.location, file, size, progressConsumer);
+ let result = await Utils.download(response.headers.location, file, size, progressConsumer);
resolve(result);
return;
}
- var stream = fs.createWriteStream(file);
+ let stream = fs.createWriteStream(file);
response.pipe(stream);
if(progressConsumer) {
@@ -156,7 +143,7 @@ class Utils {
return new Promise((resolve) => {
axios.get("https://optifine.net/adloadx?f=OptiFine_" + version + ".jar")
.then((response) => {
- var link = "https://optifine.net/downloadx?f=" +
+ let link = "https://optifine.net/downloadx?f=" +
response.data.substring(response.data
.indexOf(""))
@@ -187,12 +174,23 @@ class Utils {
}
}
+ static getNiceOsName() {
+ switch(os.type()) {
+ case "Linux":
+ return "Linux";
+ case "Darwin":
+ return "macOS";
+ case "Windows_NT":
+ return "Windows";
+ }
+ }
+
// Expands an image URL into full data url
// Heavily based around https://stackoverflow.com/a/64929732
static expandImageURL(url) {
return new Promise(async(resolve) => {
- var data = await fetch(url);
- var reader = new FileReader();
+ let data = await fetch(url);
+ let reader = new FileReader();
reader.readAsDataURL(await data.blob());
reader.onloadend = () => {
resolve(reader.result);
@@ -206,7 +204,7 @@ class Utils {
static loadImage(url) {
return new Promise(async(resolve) => {
- var image = new Image();
+ let image = new Image();
image.onload = () => {
resolve(image);
};
@@ -216,13 +214,13 @@ class Utils {
static getTextures(uuid) {
return new Promise(async(resolve) => {
- var profile = await axios.get("https://sessionserver.mojang.com/session/minecraft/profile/" + uuid);
+ let profile = await axios.get("https://sessionserver.mojang.com/session/minecraft/profile/" + uuid);
if(!profile.data) {
return;
}
else {
- for(var property of profile.data.properties) {
+ for(let property of profile.data.properties) {
if(property.name == "textures") {
resolve(JSON.parse(atob(property.value)).textures);
return;