Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
20 changes: 20 additions & 0 deletions .github/workflows/privacy-scan.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: Privacy Scan

permissions:
contents: read

on:
pull_request:
push:
branches:
- main

jobs:
scan:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Scan for sensitive usernames and local paths
run: bash scripts/check-sensitive-paths.sh
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,8 @@ run/
*.iml
.idea/
out/

# Release jars under prebuilt/ only (not arbitrary *.jar elsewhere).
prebuilt/*
!prebuilt/.gitkeep
!prebuilt/*.jar
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ LegacyLink is a **server-side only** Fabric mod. Install these on the **dedicate
| Requirement | Role | Version this repo is built against |
|-------------|------|-------------------------------------|
| **Java** | JVM for the server | **25+** (see `build.gradle.kts` toolchain) |
| **Minecraft** | Game / protocol | **`26.2-snapshot-2`** (`gradle.properties` → `minecraft_version`) |
| **Minecraft** | Game / protocol | **`26.2-snapshot-3`** (`gradle.properties` → `minecraft_version`) |
| **Fabric Loader** | Mod bootstrap | **`>= 0.19.1`** (`fabricloader` in `fabric.mod.json`) |
| **Fabric API** | Library jar (`fabric-api` on Modrinth / Maven). Used at runtime (e.g. `ServerLifecycleEvents`). | **`>= 0.145.5`** — compile against **`0.145.5+26.2`** (`gradle.properties` → `fabric_version`). Use the **Fabric API build that matches your exact 26.2 snapshot** if yours differs. |
| **MixinExtras** | Embedded in the LegacyLink jar (jar-in-jar). Registers `@WrapMethod` and related injectors at preLaunch; **not** a separate mod to install. | **0.5.3** (`build.gradle.kts`, `io.github.llamalad7:mixinextras-fabric`) |
Expand Down Expand Up @@ -75,7 +75,7 @@ cp prebuilt/legacylink-0.1.1.jar /path/to/server/mods/

### Known limitations (v0.1)

- **Block state ID ceiling** (`MAX_26_1_BLOCKSTATE_ID` in `LegacyLinkConstants`) is the **inclusive** last index the legacy client accepts (`0..MAX`). Pinned to **26.1.1** (`30207`) so 26.2 palette IDs remap safely; setting it too high can crash legacy clients in `LinearPalette.read`.
- **Mapping source of truth:** block states and items are translated from bundled 26.1.2/26.2-snapshot-3 dump tables by exact ID membership, not by "max legacy ID" thresholds.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Align snapshot version wording across README sections.

Line 78 says mappings are based on 26.2-snapshot-3, but Line 38 still says the repo is built against 26.2-snapshot-2. Please make these consistent to avoid operator confusion.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` at line 78, The README has inconsistent snapshot version mentions:
update all occurrences so they match (either change "26.2-snapshot-2" to
"26.2-snapshot-3" or change "26.1.2/26.2-snapshot-3" to use "26.2-snapshot-2");
search for the exact strings "26.2-snapshot-2" and "26.2-snapshot-3" (and
"26.1.2/26.2-snapshot-3") and make them uniform across the file (for example
replace "26.2-snapshot-2" with "26.2-snapshot-3" if the repo is actually built
against snapshot-3), ensuring the Mapping source of truth line and the repo
build line contain the same snapshot identifier.

- **Item stacks (wire):** 26.2↔26.1 numeric item ids for legacy connections via `LegacyItemIdTable` + `ItemStackOptionalCodecMixin` (optional stacks, including creative untrusted codec) and `HashedStackActualItemMixin` (container clicks). Inbound decode is scoped to `PacketDecoder#decode` (`PacketDecoderMixin`).

## Configuration
Expand Down Expand Up @@ -127,7 +127,7 @@ This repository includes a **prebuilt** Fabric mod jar under `prebuilt/` for dir
Current file:

- `prebuilt/legacylink-0.1.1.jar`
- SHA-256: `bb0c95bb9d4f3f91caab249df9ee20ea4acfb3f7ca2f6ca236bd319a584aaea2`
- SHA-256: `c6bb0b01cb0675231938e8a62c90aaf88c6bb5fbce289e8992f93261fe57caa4`

Verify:

Expand Down
Empty file added prebuilt/.gitkeep
Empty file.
Binary file modified prebuilt/legacylink-0.1.1.jar
Binary file not shown.
42 changes: 42 additions & 0 deletions scripts/check-sensitive-paths.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#!/usr/bin/env bash
set -euo pipefail

REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "${REPO_ROOT}"

echo "Scanning for sensitive usernames/absolute local paths..."

python3 - <<'PY'
import pathlib
import re
import sys

root = pathlib.Path(".")
pattern = re.compile(r"/Users/[^/]+/|/home/[^/]+/|[A-Za-z]:\\+Users\\+[^\\]+\\+", re.IGNORECASE)
skip_suffixes = {".jar", ".png", ".jpg", ".jpeg", ".gif", ".webp"}
skip_files = {"scripts/check-sensitive-paths.sh"}
skip_prefixes = ("build/", ".gradle/")

hits = []
for path in root.rglob("*"):
if not path.is_file():
continue
rel = path.as_posix()
if rel.startswith(".git/") or rel.startswith(skip_prefixes) or rel in skip_files or path.suffix.lower() in skip_suffixes:
continue
try:
text = path.read_text(encoding="utf-8")
except Exception:
continue
for line_no, line in enumerate(text.splitlines(), start=1):
if pattern.search(line):
hits.append((rel, line_no, line.strip()))

if hits:
for rel, line_no, line in hits:
print(f"{rel}:{line_no}:<REDACTED_LINE>")
print("\nERROR: Sensitive identifier/path found. Sanitize before commit/PR.")
Comment thread
coderabbitai[bot] marked this conversation as resolved.
sys.exit(1)

print("No sensitive identifiers/paths detected.")
PY
16 changes: 6 additions & 10 deletions src/main/java/dev/ohno/legacylink/LegacyLinkConstants.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,9 @@

public final class LegacyLinkConstants {

public static final int PROTOCOL_26_1 = 775;
public static final int PROTOCOL_26_2_SNAPSHOT = 1073742132;
// Upper bounds for 26.1.x protocol registries; anything above is treated as 26.2-only on the wire.
// Block state ids in chunk palettes must be valid indices in the legacy client's {@code Block.BLOCK_STATE_REGISTRY}.
// 26.1.1 rejects {@code id 30208} (so valid indices are {@code 0..30207}). 26.2 assigns new states at 30208+,
// so keep this inclusive ceiling pinned to the lowest supported 26.1.x client to avoid palette decode crashes.
// If LegacyLink later drops 26.1.1 support, this can be re-evaluated against the newer legacy floor.
public static final int MAX_26_1_ITEM_ID = 1505;
public static final int MAX_26_1_BLOCKSTATE_ID = 30207;
// Supported bridge pair: 26.2-snapshot-3 server <-> 26.1.2 client.
public static final int PROTOCOL_26_1_2 = 775;
public static final int PROTOCOL_26_2_SNAPSHOT_3 = 1073742133;

public static final Set<String> SULFUR_BLOCK_IDS = Set.of(
"minecraft:sulfur",
Expand Down Expand Up @@ -41,7 +35,8 @@ public final class LegacyLinkConstants {
"minecraft:cinnabar_brick_slab",
"minecraft:cinnabar_brick_stairs",
"minecraft:cinnabar_brick_wall",
"minecraft:chiseled_cinnabar"
"minecraft:chiseled_cinnabar",
"minecraft:sulfur_spike"
);

public static final Set<String> SULFUR_ITEM_IDS = Set.of(
Expand Down Expand Up @@ -72,6 +67,7 @@ public final class LegacyLinkConstants {
"minecraft:cinnabar_brick_stairs",
"minecraft:cinnabar_brick_wall",
"minecraft:chiseled_cinnabar",
"minecraft:sulfur_spike",
"minecraft:sulfur_cube_bucket",
"minecraft:sulfur_cube_spawn_egg"
);
Expand Down
13 changes: 9 additions & 4 deletions src/main/java/dev/ohno/legacylink/LegacyLinkMod.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import dev.ohno.legacylink.mapping.RegistryRemapper;
import dev.ohno.legacylink.runtime.LegacyRuntimeContext;
import dev.ohno.legacylink.telemetry.TranslationStats;
import net.minecraft.SharedConstants;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
import org.slf4j.Logger;
Expand All @@ -19,9 +20,14 @@ public class LegacyLinkMod implements ModInitializer {

@Override
public void onInitialize() {
LOGGER.info("[LegacyLink] Initializing protocol bridge (26.2 -> 26.1)");
LOGGER.info("[LegacyLink] Initializing protocol bridge (26.2-snapshot-3 -> 26.1.2)");

ServerLifecycleEvents.SERVER_STARTED.register(server -> {
if (SharedConstants.getProtocolVersion() != LegacyLinkConstants.PROTOCOL_26_2_SNAPSHOT_3) {
LOGGER.warn("[LegacyLink] Disabled bridge init: server protocol {} is unsupported (expected {})",
SharedConstants.getProtocolVersion(), LegacyLinkConstants.PROTOCOL_26_2_SNAPSHOT_3);
return;
}
LegacyRuntimeContext.initialize(
server.registryAccess(),
server.overworld().palettedContainerFactory(),
Expand All @@ -31,6 +37,8 @@ public void onInitialize() {
RegistryRemapper.buildMappings();
LOGGER.info("[LegacyLink] Block/item/entity mappings built — {} block states remapped, {} items remapped",
RegistryRemapper.blockStateRemapCount(), RegistryRemapper.itemRemapCount());
LOGGER.info("[LegacyLink] Ready — 26.1.2 clients (protocol {}) on 26.2-snapshot-3 server (protocol {})",
LegacyLinkConstants.PROTOCOL_26_1_2, LegacyLinkConstants.PROTOCOL_26_2_SNAPSHOT_3);
});

ServerLifecycleEvents.SERVER_STOPPING.register(server -> {
Expand All @@ -39,9 +47,6 @@ public void onInitialize() {
TranslationStats.dump();
});

LOGGER.info("[LegacyLink] Ready — 26.1 clients (protocol {}) will be accepted",
LegacyLinkConstants.PROTOCOL_26_1);

if (PositionPacketTrace.enabled()) {
LOGGER.warn("[LegacyLink] Position tracing ON (-Dlegacylink.tracePositions=true). "
+ "stage=connection_send (26.2) vs stage=post_legacy_rewrite (26.1 after translation); "
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import net.minecraft.network.syncher.SynchedEntityData;
import net.minecraft.network.protocol.game.ClientboundTeleportEntityPacket;
import net.minecraft.network.protocol.game.ClientboundUpdateAttributesPacket;
import net.minecraft.network.protocol.common.ClientboundDisconnectPacket;
import net.minecraft.resources.Identifier;
import net.minecraft.world.phys.Vec3;
import org.jspecify.annotations.Nullable;
Expand Down Expand Up @@ -139,9 +140,17 @@ private static String summarize(Packet<?> packet) {
if (packet instanceof ClientboundMoveEntityPacket p) {
int eid = moveEntityId(p);
return String.format(java.util.Locale.ROOT,
"entityId=%d dPos=(%d,%d,%d) yRot=%d xRot=%d hasPos=%s hasRot=%s onGround=%s wireType=%s",
eid, p.getXa(), p.getYa(), p.getZa(), p.getYRot(), p.getXRot(),
p.hasPosition(), p.hasRotation(), p.isOnGround(), p.type().id());
"entityId=%d dPos=(%s,%s,%s) yRot=%s xRot=%s hasPos=%s hasRot=%s onGround=%s wireType=%s",
eid,
p.getXa(),
p.getYa(),
p.getZa(),
p.getYRot(),
p.getXRot(),
p.hasPosition(),
p.hasRotation(),
p.isOnGround(),
p.type().id());
}
if (packet instanceof ClientboundRotateHeadPacket p) {
return "entityId=" + rotateHeadEntityId(p) + " yHeadRot=" + p.getYHeadRot();
Expand All @@ -158,6 +167,16 @@ private static String summarize(Packet<?> packet) {
if (packet instanceof ClientboundLevelChunkWithLightPacket) {
return "chunk_with_light (payload omitted)";
}
if (packet instanceof ClientboundDisconnectPacket p) {
String text;
try {
text = p.reason() == null ? "<null>" : p.reason().getString();
} catch (RuntimeException e) {
text = "<unreadable:" + e.getClass().getSimpleName() + ">";
}
String sanitized = text.replace('\r', ' ').replace('\n', ' ');
return "reason=\"" + sanitized + "\"";
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return "-";
}

Expand Down
Loading
Loading