Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
f10fc9a
Fix local offline skins in multiplayer servers via local proxy server…
google-labs-jules[bot] Jul 25, 2026
3a62c19
Fix offline player skins on multiplayer servers by supporting determi…
google-labs-jules[bot] Jul 25, 2026
9d6cbbb
Implement own Ely.fly skin system type and development under ELY.FLY …
google-labs-jules[bot] Jul 25, 2026
ff8c913
Implement inbuilt skin-loader mod connection from launcher skin folde…
google-labs-jules[bot] Jul 26, 2026
99a2b4c
Fully integrate CraftynMC account login, registration, and skin dynam…
google-labs-jules[bot] Jul 26, 2026
cf1d7bc
Clean up login selection to support exactly Microsoft, CraftynMC, and…
google-labs-jules[bot] Jul 26, 2026
6a6c202
Create ic_auth_fearnet.xml
minetwice Jul 27, 2026
0810aaa
Create fragment_fearnet_login.xml
minetwice Jul 27, 2026
10393b1
Update authentication buttons in layout XML
minetwice Jul 27, 2026
0263c8f
Update ExtraConstants.java
minetwice Jul 27, 2026
fa6b3c6
Create FearNetLoginFragment.java
minetwice Jul 27, 2026
dc9fd24
Create SelectAuthFragment
minetwice Jul 27, 2026
af745c0
Update AuthType.java
minetwice Jul 27, 2026
6a6aa1b
Create FearNetAuthResponse.java
minetwice Jul 27, 2026
a32dcc1
Create FearNetBackgroundLogin.java
minetwice Jul 27, 2026
e2321b1
Update AccountSpinner.java
minetwice Jul 27, 2026
d62903b
Update AccountSpinner.java
minetwice Jul 27, 2026
91a8f78
Configure strictly Microsoft, CraftynMC, and Local logins with premiu…
google-labs-jules[bot] Jul 27, 2026
56dd0e3
Update fragment_select_auth_method.xml
minetwice Jul 27, 2026
bb029e0
Update SelectAuthFragment.java
minetwice Jul 27, 2026
c986e66
Configure strictly Microsoft, CraftynMC, and Local logins with premiu…
google-labs-jules[bot] Jul 27, 2026
f99af35
Live-connect CraftynMC login with built-in WebView and automate dynam…
google-labs-jules[bot] Jul 28, 2026
7c3580c
Refactor Craftyn login layout for improved design
minetwice Jul 30, 2026
f9ff552
Refactor CraftynLoginFragment for new login UI
minetwice Jul 30, 2026
9324e6f
Fix CraftynMC skin URL for authentication
minetwice Jul 30, 2026
bf80514
Add CraftynAuthResponse model for authentication
minetwice Jul 30, 2026
340488c
Update CraftynBackgroundLogin.java
minetwice Jul 30, 2026
ef2a4b4
Fully connect launcher to CraftynMC website with native login, regist…
google-labs-jules[bot] Jul 30, 2026
ecec5bc
Resolve in-game skin display failure by mapping custom textures to wh…
google-labs-jules[bot] Jul 30, 2026
3bcff53
fix(skins): generate valid texture hashes and add skin proxying
google-labs-jules[bot] Jul 30, 2026
92b656d
Add files via upload
minetwice Jul 30, 2026
a29d3aa
fix(skins): secure texture protocol, auto-sync skin pack, and parse s…
google-labs-jules[bot] Jul 30, 2026
be19246
Fix in-game skin synchronization and local skin server proxy for mult…
google-labs-jules[bot] Jul 30, 2026
3b17a1e
Fix custom skin loading on multiplayer servers via full Yggdrasil API…
google-labs-jules[bot] Jul 31, 2026
a03db6b
Add files via upload
minetwice Jul 31, 2026
e4d413c
Fix skin loading on multiplayer servers and launcher crash
google-labs-jules[bot] Jul 31, 2026
905a72c
Fix multiplayer skin synchronization, local signature generation, and…
google-labs-jules[bot] Jul 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions CRAFTYN_MC_WEBSITE_CHANGES.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# CraftynMC Website Integration Tutorial (Express Backend Changes)
==================================================================

To support the live launcher-side username availability check, you should add a lightweight public endpoint to your Express/Node.js backend. This endpoint will be queried in real-time by the launcher as the player types their username.

### Step 1: Open your backend server code (e.g., `server.js` or `app.js`)

Locate where your routes are registered (typically near your `/login` or `/register` route definitions).

### Step 2: Paste the following `/api/check-username` endpoint code:

```javascript
// Live Username Availability Check Endpoint
app.get('/api/check-username', async (req, res) => {
try {
const username = req.query.username;
if (!username) {
return res.status(400).json({ error: "Username parameter is required." });
}

// Search your database for an existing user with the same username (case-insensitive)
// Assuming you are using MongoDB/Mongoose:
const existingUser = await User.findOne({
username: { $regex: new RegExp("^" + username + "$", "i") }
});

if (existingUser) {
// Username is already registered and taken
return res.json({ available: false, message: "Username is already taken." });
} else {
// Username is free and available to register
return res.json({ available: true, message: "Username is available!" });
}
} catch (error) {
console.error("Error in check-username:", error);
res.status(500).json({ error: "Internal server error." });
}
});
```

### Step 3: Redeploy your backend on Render!

Once redeployed, the launcher will dynamically query `https://farmer-my1t.onrender.com/api/check-username?username={username}` in the background and show the live green "✓ Username Available" status!
289 changes: 289 additions & 0 deletions WEBSITE_SKIN_PROXY_CHANGES.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,289 @@
# CraftynMC / FEAR Network Yggdrasil API Server Guide
=====================================================

This guide shows you exactly how to write or update your backend code (Node.js/Express) at `https://farmer-my1t.onrender.com/` to make skins show up perfectly on MULTIPLAYER servers!

The game client (using authlib-injector) sends all skin queries and multiplayer session verifications directly to your website. You must implement three core Yggdrasil endpoints:
1. `/sessionserver/session/minecraft/join` (Client tells server they are joining)
2. `/sessionserver/session/minecraft/hasJoined` (Server asks your API if the client actually joined)
3. `/sessionserver/session/minecraft/profile/:uuid` (Retrieves player profile/skin)

Here is the complete, production-ready implementation of these endpoints using your website's private key for signing.

---

## 1. Prerequisites (Setup RSA Keys on your Server)

Authlib-injector requires all profile responses to be signed with the website's custom private key.
Ensure you have loaded your RSA keys in your code:

```javascript
const fs = require('fs');
const crypto = require('crypto');

// Load your keys (usually loaded at server startup)
const keys = {
publicKey: fs.readFileSync('./keys/public.pem', 'utf8'),
privateKey: fs.readFileSync('./keys/private.pem', 'utf8')
};

// Simple signing function
function signPayload(privateKeyPem, data) {
const sign = crypto.createSign('SHA1WithRSA');
sign.update(data);
return sign.sign(privateKeyPem, 'base64');
}
```

---

## 2. In-Memory Session Storage (For Multiplayer Joining)

Minecraft multiplayer servers use a "handshake" to authenticate offline/injector players:
- The client contacts `/join` with their `accessToken`, `selectedProfile` (UUID), and a `serverId` (hash).
- Your API must temporarily store this server join request.
- The multiplayer server then contacts `/hasJoined?username=NAME&serverId=HASH` to verify the connection.

Add a simple global map to store these active join states:

```javascript
// Map to temporarily store active server joining sessions
const activeSessions = new Map(); // key: "username:serverId", value: { uuid, properties }
```

---

## 3. Implement the Routes in your Express App (`src/routes/yggdrasil.js`)

Replace or add these three endpoints in your routing file:

```javascript
const fetch = require("node-fetch"); // Or use Node 18+ global fetch

// ---------------------------------------------------------------------
// 1. JOIN ENDPOINT (Client-side Handshake)
// ---------------------------------------------------------------------
router.post("/sessionserver/session/minecraft/join", async (req, res) => {
try {
const { accessToken, selectedProfile, serverId } = req.body;
if (!accessToken || !selectedProfile || !serverId) {
return res.status(400).json({ error: "Bad Request", errorMessage: "Missing parameters" });
}

// Fetch user matching the selectedProfile UUID from your MongoDB database
const user = await User.findOne({ uuid: selectedProfile });
if (!user) {
return res.status(401).json({ error: "Forbidden", errorMessage: "Invalid profile uuid" });
}

// Save session details temporarily
const sessionKey = `${user.username.toLowerCase()}:${serverId}`;
activeSessions.set(sessionKey, {
uuid: selectedProfile.replace(/-/g, "").toLowerCase(),
username: user.username,
userRecord: user
});

// Clean up session automatically after 30 seconds to prevent memory leaks
setTimeout(() => {
activeSessions.delete(sessionKey);
}, 30000);

console.log(`[Yggdrasil] Join registered for ${user.username} on serverId: ${serverId}`);
return res.status(204).end();
} catch (err) {
console.error("Error in join route:", err);
return res.status(500).json({ error: "Internal Server Error" });
}
});

// ---------------------------------------------------------------------
// 2. HASJOINED ENDPOINT (Server-side Verification)
// ---------------------------------------------------------------------
router.get("/sessionserver/session/minecraft/hasJoined", async (req, res) => {
try {
const { username, serverId } = req.query;
if (!username || !serverId) {
return res.status(400).end();
}

const sessionKey = `${username.toLowerCase()}:${serverId}`;
const session = activeSessions.get(sessionKey);

if (session) {
// Player successfully verified! Build their signed profile response
console.log(`[Yggdrasil] Server successfully verified connection of ${username}`);
activeSessions.delete(sessionKey); // Consume session

const compactUuid = session.uuid;
const properties = [];

// Build skin textures property if user has a custom skin
const texturesPayload = {
timestamp: Date.now(),
profileId: compactUuid,
profileName: session.username,
textures: {
SKIN: {
url: `https://farmer-my1t.onrender.com/skins/${session.username}.png`
}
}
};

// Set skin model if slim/Alex
if (session.userRecord.skinModel === "slim") {
texturesPayload.textures.SKIN.metadata = { model: "slim" };
}

const valBase64 = Buffer.from(JSON.stringify(texturesPayload)).toString("base64");
const signature = signPayload(keys.privateKey, valBase64);

properties.push({
name: "textures",
value: valBase64,
signature: signature
});

return res.json({
id: compactUuid,
name: session.username,
properties: properties
});
}

// If not found locally, fallback to proxy verifying with Mojang's official servers!
console.log(`[Yggdrasil] Session not found locally. Fallback proxy checking Mojang for: ${username}`);
const mojangRes = await fetch(`https://sessionserver.mojang.com/session/minecraft/hasJoined?username=${username}&serverId=${serverId}`);

if (mojangRes.status === 200) {
const mojangSession = await mojangRes.json();

// We must resign the premium player's texture with OUR private key
// because authlib-injector client only accepts signatures from our server!
const resignedProperties = [];
if (mojangSession.properties) {
for (const prop of mojangSession.properties) {
if (prop.name === "textures") {
const val = prop.value;
const signature = signPayload(keys.privateKey, val);
resignedProperties.push({
name: "textures",
value: val,
signature: signature
});
} else {
resignedProperties.push(prop);
}
}
}

return res.json({
id: mojangSession.id,
name: mojangSession.name,
properties: resignedProperties
});
}

return res.status(204).end();
} catch (err) {
console.error("Error in hasJoined route:", err);
return res.status(204).end();
}
});

// ---------------------------------------------------------------------
// 3. PROFILE ENDPOINT (Query skins & details by UUID)
// ---------------------------------------------------------------------
router.get("/sessionserver/session/minecraft/profile/:uuid", async (req, res) => {
try {
const compact = req.params.uuid.replace(/-/g, "").toLowerCase();

// Format dashed UUID for database comparison if needed
const dashed = [
compact.substring(0, 8),
compact.substring(8, 12),
compact.substring(12, 16),
compact.substring(16, 20),
compact.substring(20, 32),
].join("-");

// 1. Check database for local CraftynMC player
const user = await User.findOne({ $or: [{ uuid: dashed }, { uuid: compact }] });
if (user) {
const properties = [];

const texturesPayload = {
timestamp: Date.now(),
profileId: compact,
profileName: user.username,
textures: {
SKIN: {
url: `https://farmer-my1t.onrender.com/skins/${user.username}.png`
}
}
};

if (user.skinModel === "slim") {
texturesPayload.textures.SKIN.metadata = { model: "slim" };
}

const valBase64 = Buffer.from(JSON.stringify(texturesPayload)).toString("base64");
const signature = signPayload(keys.privateKey, valBase64);

properties.push({
name: "textures",
value: valBase64,
signature: signature
});

return res.json({
id: compact,
name: user.username,
properties: properties
});
}

// 2. Fallback: Query official Mojang server if skin/profile belongs to a premium account
console.log(`[Yggdrasil] Profile not found locally. Proxying profile lookup to Mojang for UUID: ${compact}`);
const mojangRes = await fetch(`https://sessionserver.mojang.com/session/minecraft/profile/${compact}?unsigned=false`);

if (mojangRes.status === 200) {
const mojangProfile = await mojangRes.json();
const resignedProperties = [];

if (mojangProfile.properties) {
for (const prop of mojangProfile.properties) {
if (prop.name === "textures") {
const val = prop.value;
const signature = signPayload(keys.privateKey, val);
resignedProperties.push({
name: "textures",
value: val,
signature: signature
});
} else {
resignedProperties.push(prop);
}
}
}

return res.json({
id: mojangProfile.id,
name: mojangProfile.name,
properties: resignedProperties
});
}

return res.status(204).end();
} catch (err) {
console.error("Error in profile route:", err);
return res.status(204).end();
}
});
```

---

## Why these changes solve the skin display on servers completely:
1. **Join & hasJoined are required for Multiplayer:** Minecraft clients verify they own the account before the multiplayer server allows them to spawn in. Without `/join` and `/hasJoined`, players can spawn as Steve/Alex or fail authentication entirely.
2. **Signature Verification Passing:** Your client's Authlib-Injector ONLY trusts signatures signed by your server's custom private key. When we fetch a premium skin from Mojang, we must parse the payload, extract the skin, and **sign it with your server private key**. This tells the client "yes, this skin is verified and safe to render."
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ public boolean onValueSet(String key, @NonNull String value) {

/* Login listeners */
private final ExtraListener<String> mMicrosoftLoginListener = new LoginExtraListener(AuthType.MICROSOFT);
private final ExtraListener<String> mElyByLoginListener = new LoginExtraListener(AuthType.ELY_BY);
private final ExtraListener<String> mCraftynLoginListener = new LoginExtraListener(AuthType.CRAFTYN_MC);
private final ExtraListener<String[]> mMojangLoginListener = (key, value) -> {
try {
MinecraftAccount minecraftAccount = Accounts.create(acc-> acc.username = value[0]);
Expand Down Expand Up @@ -136,7 +136,7 @@ private void init() {

ExtraCore.addExtraListener(ExtraConstants.MOJANG_LOGIN_TODO, mMojangLoginListener);
ExtraCore.addExtraListener(ExtraConstants.MICROSOFT_LOGIN_TODO, mMicrosoftLoginListener);
ExtraCore.addExtraListener(ExtraConstants.ELYBY_LOGIN_TODO, mElyByLoginListener);
ExtraCore.addExtraListener(ExtraConstants.ELYFLY_LOGIN_TODO, mCraftynLoginListener);
ExtraCore.addExtraListener(ExtraConstants.REFRESH_ACCOUNT_SPINNER, mRefreshAccountsListener);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import com.google.gson.annotations.SerializedName;

import net.kdt.pojavlaunch.authenticator.impl.ElyByBackgroundLogin;
import net.kdt.pojavlaunch.authenticator.impl.MicrosoftBackgroundLogin;

import git.artdeell.mojo.R;
Expand All @@ -15,12 +14,12 @@ public enum AuthType {
null,
"https://mineskin.eu/skin/%s" // Switched from mc-heads.net cause blocked in Russia
),
@SerializedName("elyby")
ELY_BY(
ElyByBackgroundLogin.CREATOR,
R.drawable.ic_auth_elyby,
"ely.by",
"http://skinsystem.ely.by/skins/%s.png"
@SerializedName("craftynmc")
CRAFTYN_MC(
net.kdt.pojavlaunch.authenticator.impl.CraftynBackgroundLogin.CREATOR,
R.drawable.ic_auth_craftynmc,
"https://farmer-my1t.onrender.com",
"https://farmer-my1t.onrender.com/skins/%s.png"
),
@SerializedName("local")
LOCAL(null, 0, null, null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,12 @@ protected MinecraftAccount() {}
public void updateSkinFace() {
String skinFaceUrlTemplate = authType.skinUrl;
if(skinFaceUrlTemplate == null) return;
String skinFaceUrl = String.format(skinFaceUrlTemplate, username);
String skinFaceUrl;
if (authType == AuthType.CRAFTYN_MC) {
skinFaceUrl = "https://farmer-my1t.onrender.com/skins/" + profileId + ".png";
} else {
skinFaceUrl = String.format(skinFaceUrlTemplate, username);
}
try {
Log.i("SkinLoader", "Updating skin face...");
File skinFile = getSkinFaceFile();
Expand Down
Loading
Loading