mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
Merge remote-tracking branch 'origin/master' into feat/optimization
# Conflicts: # tools/run-creative-e2e.sh
This commit is contained in:
commit
22e987070a
106 changed files with 20031 additions and 3930 deletions
|
|
@ -135,6 +135,44 @@ Data source: `https://raw.githubusercontent.com/PrismarineJS/minecraft-data/mast
|
|||
|
||||
Uses `curl` with resume (`-C -`) for reliable download over slow connections. Falls back to manual download if retries are exhausted.
|
||||
|
||||
## gen_block_color_map.py -- Generate minimap block color JSON
|
||||
|
||||
Extracts block-to-MapColor RGB mappings from decompiled Minecraft source for the TUI minimap.
|
||||
|
||||
```bash
|
||||
python3 tools/gen_block_color_map.py MinecraftOfficial/26.1-rc-2-decompiled
|
||||
# -> MinecraftClient/Tui/MinimapBlockColors.json
|
||||
```
|
||||
|
||||
Parses three files from the decompiled source:
|
||||
- `MapColor.java` -- extracts the 64 base MapColor constants and their RGB values
|
||||
- `DyeColor.java` -- maps dye colors to MapColor constants
|
||||
- `Blocks.java` -- determines each block's assigned MapColor via `.mapColor()` calls
|
||||
|
||||
Output: `MinecraftClient/Tui/MinimapBlockColors.json` (embedded as a resource via `.csproj`). Contains color entries, plus lists of transparent, water, and ice materials.
|
||||
|
||||
Validates each block name against MCC's `Material.cs` enum. Blocks without a matching enum value are skipped.
|
||||
|
||||
## gen_entity_category_map.py -- Generate minimap entity category JSON
|
||||
|
||||
Extracts entity-to-MobCategory mappings from decompiled Minecraft source for the TUI minimap.
|
||||
|
||||
```bash
|
||||
python3 tools/gen_entity_category_map.py MinecraftOfficial/26.1-rc-2-decompiled
|
||||
# -> MinecraftClient/Tui/MinimapEntityCategories.json
|
||||
```
|
||||
|
||||
Parses `EntityType.java` to read each entity's `MobCategory` assignment from the `EntityType.Builder.of(Factory, MobCategory.XXX)` call. Maps Minecraft categories to MCC minimap categories:
|
||||
- `MONSTER` -> hostile
|
||||
- `CREATURE`/`AMBIENT`/`AXOLOTLS`/`WATER_*` -> passive
|
||||
- `MISC` -> non_living
|
||||
|
||||
The script maintains manual override lists for:
|
||||
- **Neutral mobs** (e.g. Enderman, Spider, Wolf, Bee) -- Minecraft has no "neutral" category; these are MONSTER or CREATURE in code but only attack when provoked
|
||||
- **Passive overrides** (e.g. Villager, WanderingTrader) -- classified as MISC in Minecraft for spawning reasons but should appear as passive on the minimap
|
||||
|
||||
Output: `MinecraftClient/Tui/MinimapEntityCategories.json` (embedded as a resource via `.csproj`). Validates each entity name against MCC's `EntityType.cs` enum.
|
||||
|
||||
## Recommended workflow
|
||||
|
||||
1. Generate server reports (Step 0)
|
||||
|
|
@ -145,6 +183,9 @@ Uses `curl` with resume (`-C -`) for reliable download over slow connections. Fa
|
|||
- Entities: `gen_entity_palette.py`
|
||||
- Metadata: `gen_entity_metadata_palette.py`
|
||||
4. Update block collision shapes: `gen_block_shapes.py`
|
||||
5. Add any missing enum values to `ItemType.cs`, `Material.cs`, `EntityType.cs`, `EntityMetaDataType.cs`
|
||||
6. Update version routing (see SKILL.md)
|
||||
7. Build and test
|
||||
5. Update minimap data (if blocks or entities changed):
|
||||
- Block colors: `gen_block_color_map.py`
|
||||
- Entity categories: `gen_entity_category_map.py`
|
||||
6. Add any missing enum values to `ItemType.cs`, `Material.cs`, `EntityType.cs`, `EntityMetaDataType.cs`
|
||||
7. Update version routing (see SKILL.md)
|
||||
8. Build and test
|
||||
|
|
|
|||
|
|
@ -86,18 +86,90 @@ fi
|
|||
|
||||
mkdir -p "$MC_OFFICIAL/remapped_jar"
|
||||
|
||||
# --- Resolve version metadata from Mojang manifest ---
|
||||
MANIFEST_URL="https://launchermeta.mojang.com/mc/game/version_manifest_v2.json"
|
||||
VERSION_URL=$(curl -sL "$MANIFEST_URL" | python3 -c "
|
||||
import json, sys
|
||||
data = json.load(sys.stdin)
|
||||
for v in data['versions']:
|
||||
if v['id'] == '$VERSION':
|
||||
print(v['url'])
|
||||
break
|
||||
")
|
||||
if [[ -z "$VERSION_URL" ]]; then
|
||||
echo "Error: version $VERSION not found in Mojang launcher manifest."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION_META=$(curl -sL "$VERSION_URL")
|
||||
MAPPING_KEY="${SIDE_LOWER}_mappings"
|
||||
HAS_MAPPINGS=$(echo "$VERSION_META" | python3 -c "
|
||||
import json, sys
|
||||
data = json.load(sys.stdin)
|
||||
print('true' if '$MAPPING_KEY' in data.get('downloads', {}) else 'false')
|
||||
")
|
||||
|
||||
echo "=== Decompiling Minecraft $VERSION ($SIDE) ==="
|
||||
echo " Remapped JAR: $REMAPPED_JAR"
|
||||
echo " Decompiled: $DECOMPILED_DIR"
|
||||
echo " Obfuscated: $HAS_MAPPINGS"
|
||||
echo ""
|
||||
|
||||
cd "$MC_OFFICIAL"
|
||||
java -jar "$DECOMPILER_JAR" \
|
||||
--version "$VERSION" \
|
||||
--side "$SIDE" \
|
||||
--decompile \
|
||||
--output "$REMAPPED_JAR" \
|
||||
--decompiled-output "$DECOMPILED_DIR"
|
||||
|
||||
if [[ "$HAS_MAPPINGS" == "true" ]]; then
|
||||
# Obfuscated version: use --version/--side to auto-download jar + mappings + deobfuscate
|
||||
java -jar "$DECOMPILER_JAR" \
|
||||
--version "$VERSION" \
|
||||
--side "$SIDE" \
|
||||
--decompile \
|
||||
--output "$REMAPPED_JAR" \
|
||||
--decompiled-output "$DECOMPILED_DIR"
|
||||
else
|
||||
# Unobfuscated version (26.1+): download jar, extract inner jar from bundle, decompile directly.
|
||||
# MinecraftDecompiler requires --mapping-path with --input, but unobfuscated versions
|
||||
# have no mappings. We use Vineflower directly instead.
|
||||
echo "No Proguard mappings for $VERSION; decompiling without deobfuscation."
|
||||
|
||||
JAR_URL=$(echo "$VERSION_META" | python3 -c "
|
||||
import json, sys
|
||||
data = json.load(sys.stdin)
|
||||
print(data['downloads']['${SIDE_LOWER}']['url'])
|
||||
")
|
||||
ORIGINAL_JAR="$MC_OFFICIAL/remapped_jar/${VERSION}-${SIDE_LOWER}-original.jar"
|
||||
if [[ ! -f "$ORIGINAL_JAR" ]]; then
|
||||
echo "Downloading ${SIDE_LOWER}.jar ..."
|
||||
curl -L -o "$ORIGINAL_JAR" "$JAR_URL"
|
||||
fi
|
||||
|
||||
# Since 1.18, server.jar is a bundled jar containing the actual game jar inside
|
||||
# META-INF/versions/<ver>/server-<ver>.jar. Extract it if present.
|
||||
DECOMPILE_TARGET="$ORIGINAL_JAR"
|
||||
EXTRACT_DIR=$(mktemp -d)
|
||||
trap "rm -rf '$EXTRACT_DIR'" EXIT
|
||||
if unzip -q -o "$ORIGINAL_JAR" "META-INF/versions.list" -d "$EXTRACT_DIR" 2>/dev/null; then
|
||||
INNER_PATH=$(awk '{print $NF}' "$EXTRACT_DIR/META-INF/versions.list" | head -1)
|
||||
if [[ -n "$INNER_PATH" ]]; then
|
||||
unzip -q -o "$ORIGINAL_JAR" "META-INF/versions/$INNER_PATH" -d "$EXTRACT_DIR"
|
||||
DECOMPILE_TARGET="$EXTRACT_DIR/META-INF/versions/$INNER_PATH"
|
||||
echo "Extracted inner jar: $INNER_PATH"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Use Vineflower directly (bundled with MinecraftDecompiler, or standalone)
|
||||
VINEFLOWER_JAR="$MC_OFFICIAL/downloads/decompiler/vineflower.jar"
|
||||
if [[ ! -f "$VINEFLOWER_JAR" ]]; then
|
||||
# Fall back to vineflower bundled inside MinecraftDecompiler's cache
|
||||
VINEFLOWER_JAR=$(find "$MC_OFFICIAL" -name "vineflower*.jar" -not -name "MinecraftDecompiler.jar" 2>/dev/null | head -1)
|
||||
fi
|
||||
if [[ -z "$VINEFLOWER_JAR" || ! -f "$VINEFLOWER_JAR" ]]; then
|
||||
echo "Error: vineflower.jar not found. Place it at $MC_OFFICIAL/downloads/decompiler/vineflower.jar"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Decompiling with Vineflower: $VINEFLOWER_JAR"
|
||||
java -jar "$VINEFLOWER_JAR" "$DECOMPILE_TARGET" "$DECOMPILED_DIR"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Done ==="
|
||||
|
|
@ -108,29 +180,18 @@ if [[ "$SIDE" == "SERVER" ]]; then
|
|||
DOWNLOADS_DIR="$MC_OFFICIAL/downloads/$VERSION"
|
||||
if [[ ! -f "$DOWNLOADS_DIR/server.jar" ]]; then
|
||||
mkdir -p "$DOWNLOADS_DIR"
|
||||
# MinecraftDecompiler downloads the original jar into its cache;
|
||||
# extract it from the bundled remapped jar or re-download via manifest.
|
||||
echo ""
|
||||
echo "Downloading server.jar for $VERSION into $DOWNLOADS_DIR ..."
|
||||
MANIFEST_URL="https://launchermeta.mojang.com/mc/game/version_manifest_v2.json"
|
||||
VERSION_URL=$(curl -sL "$MANIFEST_URL" | python3 -c "
|
||||
import json, sys
|
||||
data = json.load(sys.stdin)
|
||||
for v in data['versions']:
|
||||
if v['id'] == '$VERSION':
|
||||
print(v['url'])
|
||||
break
|
||||
")
|
||||
if [[ -n "$VERSION_URL" ]]; then
|
||||
SERVER_JAR_URL=$(curl -sL "$VERSION_URL" | python3 -c "
|
||||
SERVER_JAR_URL=$(echo "$VERSION_META" | python3 -c "
|
||||
import json, sys
|
||||
data = json.load(sys.stdin)
|
||||
print(data['downloads']['server']['url'])
|
||||
")
|
||||
if [[ -n "$SERVER_JAR_URL" ]]; then
|
||||
curl -L -o "$DOWNLOADS_DIR/server.jar" "$SERVER_JAR_URL"
|
||||
echo "Downloaded server.jar"
|
||||
else
|
||||
echo "Warning: could not find version $VERSION in Mojang manifest; server.jar not downloaded."
|
||||
echo "Warning: could not download server.jar for $VERSION."
|
||||
fi
|
||||
else
|
||||
echo "server.jar already exists: $DOWNLOADS_DIR/server.jar"
|
||||
|
|
|
|||
268
tools/gen_block_color_map.py
Normal file
268
tools/gen_block_color_map.py
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate MinimapBlockColors.json from decompiled Minecraft source.
|
||||
|
||||
Parses MapColor.java for the 62 base map colors (ID -> RGB), then parses
|
||||
Blocks.java to extract each block's mapColor assignment, and outputs a
|
||||
JSON mapping from MCC Material enum names (PascalCase) to RGB triples.
|
||||
|
||||
Usage:
|
||||
python3 tools/gen_block_color_map.py <decompiled_root>
|
||||
|
||||
Example:
|
||||
python3 tools/gen_block_color_map.py MinecraftOfficial/26.1-rc-2-decompiled
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
OUTPUT_PATH = (Path(__file__).resolve().parent.parent
|
||||
/ "MinecraftClient" / "Tui" / "MinimapBlockColors.json")
|
||||
MATERIAL_CS = (Path(__file__).resolve().parent.parent
|
||||
/ "MinecraftClient" / "Mapping" / "Material.cs")
|
||||
|
||||
|
||||
def mc_name_to_csharp(mc_name: str) -> str:
|
||||
name = mc_name.removeprefix("minecraft:")
|
||||
return "".join(word.capitalize() for word in name.split("_"))
|
||||
|
||||
|
||||
def parse_map_colors(map_color_java: Path) -> dict[str, tuple[int, int, int]]:
|
||||
"""Parse MapColor.java: extract name -> (R, G, B) for each constant."""
|
||||
text = map_color_java.read_text()
|
||||
colors: dict[str, tuple[int, int, int]] = {}
|
||||
|
||||
pattern = re.compile(
|
||||
r'public static final MapColor\s+(\w+)\s*=\s*new\s+MapColor\(\s*(\d+)\s*,\s*(\d+)\s*\)')
|
||||
for m in pattern.finditer(text):
|
||||
name = m.group(1)
|
||||
color_int = int(m.group(3))
|
||||
r = (color_int >> 16) & 0xFF
|
||||
g = (color_int >> 8) & 0xFF
|
||||
b = color_int & 0xFF
|
||||
colors[name] = (r, g, b)
|
||||
|
||||
return colors
|
||||
|
||||
|
||||
def parse_dye_to_map_color(dye_color_java: Path) -> dict[str, str]:
|
||||
"""Parse DyeColor.java: extract DyeColor name -> MapColor name."""
|
||||
text = dye_color_java.read_text()
|
||||
mapping: dict[str, str] = {}
|
||||
|
||||
pattern = re.compile(
|
||||
r'(\w+)\(\d+,\s*"[^"]+",\s*\d+,\s*MapColor\.(\w+)')
|
||||
for m in pattern.finditer(text):
|
||||
mapping[m.group(1)] = m.group(2)
|
||||
|
||||
return mapping
|
||||
|
||||
|
||||
def extract_block_declarations(text: str) -> list[tuple[str, str, str]]:
|
||||
"""Extract (field_name, block_id, full_register_body) for each block declaration.
|
||||
|
||||
Returns list of (FIELD_NAME, "block_name", "register(...) content").
|
||||
"""
|
||||
results = []
|
||||
|
||||
# Find all "public static final Block FIELD = register(...)" declarations.
|
||||
# These span multiple lines and end with ");".
|
||||
# Strategy: find start pattern, then track parens to find matching end.
|
||||
field_pattern = re.compile(
|
||||
r'public\s+static\s+final\s+Block\s+(\w+)\s*=\s*register\s*\(')
|
||||
|
||||
pos = 0
|
||||
while pos < len(text):
|
||||
m = field_pattern.search(text, pos)
|
||||
if not m:
|
||||
break
|
||||
|
||||
field_name = m.group(1)
|
||||
paren_start = m.end() - 1 # position of opening '('
|
||||
|
||||
# Find matching closing ')' then ';'
|
||||
depth = 1
|
||||
i = paren_start + 1
|
||||
while i < len(text) and depth > 0:
|
||||
if text[i] == '(':
|
||||
depth += 1
|
||||
elif text[i] == ')':
|
||||
depth -= 1
|
||||
i += 1
|
||||
|
||||
register_body = text[paren_start:i]
|
||||
|
||||
# Extract block name string from register call
|
||||
name_match = re.search(r'(?:BlockIds\.(\w+)|"(\w+)")', register_body)
|
||||
if name_match:
|
||||
raw_id = name_match.group(1) or name_match.group(2)
|
||||
block_id = raw_id.lower() if raw_id.isupper() else raw_id
|
||||
else:
|
||||
block_id = field_name.lower()
|
||||
|
||||
results.append((field_name, block_id, register_body))
|
||||
pos = i
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def parse_blocks(blocks_java: Path, map_colors: dict[str, tuple[int, int, int]],
|
||||
dye_to_map: dict[str, str]) -> dict[str, tuple[int, int, int]]:
|
||||
"""Parse Blocks.java: extract block_name -> (R, G, B)."""
|
||||
text = blocks_java.read_text()
|
||||
|
||||
declarations = extract_block_declarations(text)
|
||||
print(f" Found {len(declarations)} block register() declarations")
|
||||
|
||||
# First pass: assign MapColor name to each block
|
||||
field_to_block_id: dict[str, str] = {}
|
||||
block_color_name: dict[str, str] = {}
|
||||
|
||||
map_color_direct = re.compile(r'\.mapColor\(MapColor\.(\w+)\)')
|
||||
map_color_dye = re.compile(r'\.mapColor\(DyeColor\.(\w+)\)')
|
||||
map_color_ref = re.compile(r'\.mapColor\((\w+)\.defaultMapColor\(\)')
|
||||
map_color_waterlogged = re.compile(r'\.mapColor\(waterloggedMapColor\(MapColor\.(\w+)\)')
|
||||
|
||||
for field_name, block_id, body in declarations:
|
||||
field_to_block_id[field_name] = block_id
|
||||
|
||||
mc = map_color_direct.search(body)
|
||||
if mc:
|
||||
block_color_name[block_id] = mc.group(1)
|
||||
continue
|
||||
|
||||
mc = map_color_dye.search(body)
|
||||
if mc:
|
||||
dye_name = mc.group(1)
|
||||
if dye_name in dye_to_map:
|
||||
block_color_name[block_id] = dye_to_map[dye_name]
|
||||
continue
|
||||
|
||||
mc = map_color_waterlogged.search(body)
|
||||
if mc:
|
||||
block_color_name[block_id] = mc.group(1)
|
||||
continue
|
||||
|
||||
mc = map_color_ref.search(body)
|
||||
if mc:
|
||||
ref_field = mc.group(1)
|
||||
ref_block = field_to_block_id.get(ref_field)
|
||||
if ref_block and ref_block in block_color_name:
|
||||
block_color_name[block_id] = block_color_name[ref_block]
|
||||
|
||||
# Second pass: resolve remaining BLOCK.defaultMapColor() references
|
||||
for field_name, block_id, body in declarations:
|
||||
if block_id in block_color_name:
|
||||
continue
|
||||
mc = map_color_ref.search(body)
|
||||
if mc:
|
||||
ref_field = mc.group(1)
|
||||
ref_block = field_to_block_id.get(ref_field)
|
||||
if ref_block and ref_block in block_color_name:
|
||||
block_color_name[block_id] = block_color_name[ref_block]
|
||||
|
||||
result: dict[str, tuple[int, int, int]] = {}
|
||||
for block_id, color_name in block_color_name.items():
|
||||
if color_name in map_colors:
|
||||
cs_name = mc_name_to_csharp(block_id)
|
||||
result[cs_name] = map_colors[color_name]
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def load_known_materials() -> set[str]:
|
||||
known = set()
|
||||
if MATERIAL_CS.exists():
|
||||
with open(MATERIAL_CS) as f:
|
||||
for line in f:
|
||||
m = re.match(r'\s+(\w+),?\s*$', line)
|
||||
if m:
|
||||
known.add(m.group(1))
|
||||
return known
|
||||
|
||||
|
||||
TRANSPARENT_BLOCKS = [
|
||||
"Air", "CaveAir", "VoidAir",
|
||||
"Glass", "GlassPane",
|
||||
"WhiteStainedGlass", "OrangeStainedGlass", "MagentaStainedGlass",
|
||||
"LightBlueStainedGlass", "YellowStainedGlass", "LimeStainedGlass",
|
||||
"PinkStainedGlass", "GrayStainedGlass", "LightGrayStainedGlass",
|
||||
"CyanStainedGlass", "PurpleStainedGlass", "BlueStainedGlass",
|
||||
"BrownStainedGlass", "GreenStainedGlass", "RedStainedGlass",
|
||||
"BlackStainedGlass",
|
||||
"WhiteStainedGlassPane", "OrangeStainedGlassPane", "MagentaStainedGlassPane",
|
||||
"LightBlueStainedGlassPane", "YellowStainedGlassPane", "LimeStainedGlassPane",
|
||||
"PinkStainedGlassPane", "GrayStainedGlassPane", "LightGrayStainedGlassPane",
|
||||
"CyanStainedGlassPane", "PurpleStainedGlassPane", "BlueStainedGlassPane",
|
||||
"BrownStainedGlassPane", "GreenStainedGlassPane", "RedStainedGlassPane",
|
||||
"BlackStainedGlassPane",
|
||||
"TintedGlass", "Barrier", "Light", "StructureVoid",
|
||||
]
|
||||
|
||||
WATER_BLOCKS = ["Water"]
|
||||
ICE_BLOCKS = ["Ice", "PackedIce", "BlueIce", "FrostedIce"]
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 2:
|
||||
print(__doc__)
|
||||
sys.exit(1)
|
||||
|
||||
root = Path(sys.argv[1])
|
||||
if not root.is_dir():
|
||||
print(f"Error: {root} is not a directory")
|
||||
sys.exit(1)
|
||||
|
||||
map_color_java = root / "net/minecraft/world/level/material/MapColor.java"
|
||||
dye_color_java = root / "net/minecraft/world/item/DyeColor.java"
|
||||
blocks_java = root / "net/minecraft/world/level/block/Blocks.java"
|
||||
|
||||
for f in [map_color_java, dye_color_java, blocks_java]:
|
||||
if not f.exists():
|
||||
print(f"Error: {f} not found")
|
||||
sys.exit(1)
|
||||
|
||||
print("Parsing MapColor.java...")
|
||||
map_colors = parse_map_colors(map_color_java)
|
||||
print(f" Found {len(map_colors)} map colors")
|
||||
|
||||
print("Parsing DyeColor.java...")
|
||||
dye_to_map = parse_dye_to_map_color(dye_color_java)
|
||||
print(f" Found {len(dye_to_map)} dye->map color mappings")
|
||||
|
||||
print("Parsing Blocks.java...")
|
||||
block_colors = parse_blocks(blocks_java, map_colors, dye_to_map)
|
||||
print(f" Extracted colors for {len(block_colors)} blocks")
|
||||
|
||||
known_materials = load_known_materials()
|
||||
if known_materials:
|
||||
matched = {k: v for k, v in block_colors.items() if k in known_materials}
|
||||
unmatched = [k for k in block_colors if k not in known_materials]
|
||||
if unmatched:
|
||||
print(f"\n {len(unmatched)} blocks not in Material.cs (will be skipped):")
|
||||
for name in sorted(unmatched)[:20]:
|
||||
print(f" {name}")
|
||||
if len(unmatched) > 20:
|
||||
print(f" ... and {len(unmatched) - 20} more")
|
||||
block_colors = matched
|
||||
print(f" {len(block_colors)} blocks matched to Material.cs entries")
|
||||
|
||||
output = {
|
||||
"version": root.name.replace("-decompiled", "").replace("-client", ""),
|
||||
"colors": {k: list(v) for k, v in sorted(block_colors.items())},
|
||||
"transparent": sorted(TRANSPARENT_BLOCKS),
|
||||
"water": WATER_BLOCKS,
|
||||
"ice": ICE_BLOCKS,
|
||||
}
|
||||
|
||||
OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(OUTPUT_PATH, 'w') as f:
|
||||
json.dump(output, f, indent=2)
|
||||
print(f"\nGenerated {OUTPUT_PATH}")
|
||||
print(f" {len(block_colors)} color entries")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
200
tools/gen_entity_category_map.py
Normal file
200
tools/gen_entity_category_map.py
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate MinimapEntityCategories.json from decompiled Minecraft source.
|
||||
|
||||
Parses EntityType.java to extract each entity's MobCategory assignment,
|
||||
then maps them to MCC minimap categories (hostile/passive/neutral/non_living).
|
||||
|
||||
Minecraft's MobCategory values:
|
||||
MONSTER -> hostile (with neutral overrides for conditionally hostile mobs)
|
||||
CREATURE -> passive (with neutral overrides for conditionally hostile mobs)
|
||||
AMBIENT -> passive
|
||||
AXOLOTLS -> passive
|
||||
WATER_CREATURE -> passive
|
||||
WATER_AMBIENT -> passive
|
||||
UNDERGROUND_WATER_CREATURE -> passive
|
||||
MISC -> non_living
|
||||
|
||||
Some mobs classified as MONSTER or CREATURE are actually "neutral" -- they
|
||||
only attack when provoked. These are listed in NEUTRAL_OVERRIDES below and
|
||||
should be updated when new conditionally-hostile mobs are added.
|
||||
|
||||
Usage:
|
||||
python3 tools/gen_entity_category_map.py <decompiled_root>
|
||||
|
||||
Example:
|
||||
python3 tools/gen_entity_category_map.py MinecraftOfficial/26.1-rc-2-decompiled
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
OUTPUT_PATH = (Path(__file__).resolve().parent.parent
|
||||
/ "MinecraftClient" / "Tui" / "MinimapEntityCategories.json")
|
||||
ENTITY_TYPE_CS = (Path(__file__).resolve().parent.parent
|
||||
/ "MinecraftClient" / "Mapping" / "EntityType.cs")
|
||||
|
||||
|
||||
def mc_name_to_csharp(mc_name: str) -> str:
|
||||
name = mc_name.removeprefix("minecraft:")
|
||||
return "".join(word.capitalize() for word in name.split("_"))
|
||||
|
||||
|
||||
# Mobs that Minecraft classifies as MONSTER or CREATURE but behave as
|
||||
# "neutral" -- they only attack when provoked. This list is maintained
|
||||
# manually because there is no machine-readable flag in the game data.
|
||||
NEUTRAL_OVERRIDES = {
|
||||
"bee", "dolphin", "goat", "iron_golem", "llama", "panda",
|
||||
"polar_bear", "snow_golem", "trader_llama", "wolf",
|
||||
"zombified_piglin", "enderman", "spider", "cave_spider",
|
||||
"copper_golem",
|
||||
}
|
||||
|
||||
# Entities whose MobCategory in the game code doesn't match how they
|
||||
# should appear on the minimap. For example, Villager and WanderingTrader
|
||||
# are MISC in MC code (for spawning reasons) but should be passive on the map.
|
||||
# ZombieHorse is MONSTER but is a rideable passive mob in practice.
|
||||
PASSIVE_OVERRIDES = {
|
||||
"villager", "wandering_trader", "zombie_horse",
|
||||
}
|
||||
|
||||
# Player has its own category in MCC -- extracted from MISC to "player".
|
||||
PLAYER_OVERRIDES = {"player"}
|
||||
|
||||
MC_TO_MCC = {
|
||||
"MONSTER": "hostile",
|
||||
"CREATURE": "passive",
|
||||
"AMBIENT": "passive",
|
||||
"AXOLOTLS": "passive",
|
||||
"WATER_CREATURE": "passive",
|
||||
"WATER_AMBIENT": "passive",
|
||||
"UNDERGROUND_WATER_CREATURE": "passive",
|
||||
"MISC": "non_living",
|
||||
}
|
||||
|
||||
|
||||
def extract_entity_categories(entity_type_java: Path) -> list[tuple[str, str, str]]:
|
||||
"""Extract (entity_id, field_name, MobCategory) from EntityType.java.
|
||||
|
||||
Returns list of (entity_id, FIELD_NAME, MobCategory_name).
|
||||
"""
|
||||
text = entity_type_java.read_text()
|
||||
results = []
|
||||
|
||||
field_pat = re.compile(
|
||||
r'public\s+static\s+final\s+EntityType<[^>]+>\s+(\w+)\s*=\s*register\s*\(')
|
||||
|
||||
pos = 0
|
||||
while pos < len(text):
|
||||
m = field_pat.search(text, pos)
|
||||
if not m:
|
||||
break
|
||||
|
||||
field_name = m.group(1)
|
||||
paren_start = m.end() - 1
|
||||
depth = 1
|
||||
i = paren_start + 1
|
||||
while i < len(text) and depth > 0:
|
||||
if text[i] == '(':
|
||||
depth += 1
|
||||
elif text[i] == ')':
|
||||
depth -= 1
|
||||
i += 1
|
||||
|
||||
body = text[paren_start:i]
|
||||
|
||||
name_match = re.search(r'"(\w+)"', body)
|
||||
entity_id = name_match.group(1) if name_match else field_name.lower()
|
||||
|
||||
cat_match = re.search(r'MobCategory\.(\w+)', body)
|
||||
mob_cat = cat_match.group(1) if cat_match else "MISC"
|
||||
|
||||
results.append((entity_id, field_name, mob_cat))
|
||||
pos = i
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def load_known_entity_types() -> set[str]:
|
||||
known = set()
|
||||
if ENTITY_TYPE_CS.exists():
|
||||
with open(ENTITY_TYPE_CS) as f:
|
||||
for line in f:
|
||||
m = re.match(r'\s+(\w+),?\s*$', line)
|
||||
if m:
|
||||
known.add(m.group(1))
|
||||
return known
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 2:
|
||||
print(__doc__)
|
||||
sys.exit(1)
|
||||
|
||||
root = Path(sys.argv[1])
|
||||
entity_type_java = root / "net/minecraft/world/entity/EntityType.java"
|
||||
|
||||
if not entity_type_java.exists():
|
||||
print(f"Error: {entity_type_java} not found")
|
||||
sys.exit(1)
|
||||
|
||||
print("Parsing EntityType.java...")
|
||||
entities = extract_entity_categories(entity_type_java)
|
||||
print(f" Found {len(entities)} entity type declarations")
|
||||
|
||||
known_types = load_known_entity_types()
|
||||
|
||||
hostile = []
|
||||
passive = []
|
||||
neutral = []
|
||||
non_living = []
|
||||
|
||||
for entity_id, field_name, mob_cat in entities:
|
||||
cs_name = mc_name_to_csharp(entity_id)
|
||||
|
||||
if known_types and cs_name not in known_types:
|
||||
continue
|
||||
|
||||
if entity_id in PLAYER_OVERRIDES:
|
||||
continue
|
||||
elif entity_id in NEUTRAL_OVERRIDES:
|
||||
neutral.append(cs_name)
|
||||
elif entity_id in PASSIVE_OVERRIDES:
|
||||
passive.append(cs_name)
|
||||
elif mob_cat in MC_TO_MCC:
|
||||
cat = MC_TO_MCC[mob_cat]
|
||||
if cat == "hostile":
|
||||
hostile.append(cs_name)
|
||||
elif cat == "passive":
|
||||
passive.append(cs_name)
|
||||
elif cat == "non_living":
|
||||
non_living.append(cs_name)
|
||||
else:
|
||||
non_living.append(cs_name)
|
||||
else:
|
||||
non_living.append(cs_name)
|
||||
|
||||
output = {
|
||||
"version": root.name.replace("-decompiled", "").replace("-client", ""),
|
||||
"hostile": sorted(hostile),
|
||||
"passive": sorted(passive),
|
||||
"neutral": sorted(neutral),
|
||||
"non_living": sorted(non_living),
|
||||
}
|
||||
|
||||
OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(OUTPUT_PATH, 'w') as f:
|
||||
json.dump(output, f, indent=2)
|
||||
|
||||
print(f"\nGenerated {OUTPUT_PATH}")
|
||||
print(f" hostile: {len(hostile)}")
|
||||
print(f" passive: {len(passive)}")
|
||||
print(f" neutral: {len(neutral)}")
|
||||
print(f" non_living: {len(non_living)}")
|
||||
print(f" total: {len(hostile) + len(passive) + len(neutral) + len(non_living)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -32,6 +32,7 @@ EOF
|
|||
VERSION="1.21.11-Vanilla"
|
||||
MODE="classic"
|
||||
PORT="25565"
|
||||
PORT_SET_BY_USER=false
|
||||
DO_BUILD=true
|
||||
DEBUG_ON=false
|
||||
FILE_INPUT=false
|
||||
|
|
@ -40,7 +41,7 @@ while [[ $# -gt 0 ]]; do
|
|||
case "$1" in
|
||||
-v|--version) VERSION="$2"; shift 2 ;;
|
||||
-m|--mode) MODE="$2"; shift 2 ;;
|
||||
-p|--port) PORT="$2"; shift 2 ;;
|
||||
-p|--port) PORT="$2"; PORT_SET_BY_USER=true; shift 2 ;;
|
||||
--no-build) DO_BUILD=false; shift ;;
|
||||
--debug-on) DEBUG_ON=true; shift ;;
|
||||
--file-input) FILE_INPUT=true; shift ;;
|
||||
|
|
@ -54,6 +55,10 @@ CFG="$TEST_ROOT/MinecraftClient.debug.ini"
|
|||
MCC_LOG="$TEST_ROOT/mcc-debug.log"
|
||||
INPUT_FILE="$REPO_ROOT/mcc_input.txt"
|
||||
SESSION_NAME="mc-${VERSION//\./_}"
|
||||
PREPARE_CFG_SCRIPT="$REPO_ROOT/.skills/mcc-integration-testing/scripts/prepare_offline_mcc_config.sh"
|
||||
ENSURE_SERVER_SCRIPT="$REPO_ROOT/.skills/mcc-integration-testing/scripts/ensure_offline_server.sh"
|
||||
PREFLIGHT_SCRIPT="$REPO_ROOT/.skills/mcc-integration-testing/scripts/preflight_test_env.sh"
|
||||
GET_PORT_SCRIPT="$REPO_ROOT/.skills/mcc-integration-testing/scripts/get_server_port.sh"
|
||||
|
||||
mkdir -p "$TEST_ROOT"
|
||||
|
||||
|
|
@ -64,6 +69,8 @@ echo " Config: $CFG"
|
|||
echo " Log: $MCC_LOG"
|
||||
echo ""
|
||||
|
||||
bash "$PREFLIGHT_SCRIPT" "$VERSION" >/dev/null
|
||||
|
||||
# --- Build ---
|
||||
if $DO_BUILD; then
|
||||
echo "[1/4] Building MCC..."
|
||||
|
|
@ -75,21 +82,22 @@ fi
|
|||
|
||||
# --- Prepare config ---
|
||||
echo "[2/4] Preparing config..."
|
||||
cp "$REPO_ROOT/MinecraftClient.ini" "$CFG"
|
||||
|
||||
sed -i \
|
||||
-e 's/Account = { Login = "[^"]*", Password = "[^"]*" }/Account = { Login = "CursorBot", Password = "-" }/' \
|
||||
-e 's/TerrainAndMovements = false/TerrainAndMovements = true/' \
|
||||
-e 's/InventoryHandling = false/InventoryHandling = true/' \
|
||||
-e 's/EntityHandling = false/EntityHandling = true/' \
|
||||
"$CFG"
|
||||
bash "$PREPARE_CFG_SCRIPT" "$CFG" "${VERSION%-Vanilla}" CursorBot >/dev/null
|
||||
|
||||
if [[ "$MODE" == "tui" ]]; then
|
||||
sed -i 's/ConsoleMode = "classic"/ConsoleMode = "tui"/' "$CFG"
|
||||
if [[ "$(uname)" == "Darwin" ]]; then
|
||||
sed -i '' 's/ConsoleMode = "classic"/ConsoleMode = "tui"/' "$CFG"
|
||||
else
|
||||
sed -i 's/ConsoleMode = "classic"/ConsoleMode = "tui"/' "$CFG"
|
||||
fi
|
||||
fi
|
||||
|
||||
if $DEBUG_ON; then
|
||||
sed -i 's/DebugMessages = false/DebugMessages = true/' "$CFG"
|
||||
if [[ "$(uname)" == "Darwin" ]]; then
|
||||
sed -i '' 's/DebugMessages = false/DebugMessages = true/' "$CFG"
|
||||
else
|
||||
sed -i 's/DebugMessages = false/DebugMessages = true/' "$CFG"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo " Config ready"
|
||||
|
|
@ -99,14 +107,7 @@ echo "[3/4] Starting server $VERSION..."
|
|||
if tmux has-session -t "$SESSION_NAME" 2>/dev/null; then
|
||||
echo " Server already running"
|
||||
else
|
||||
# Ensure offline mode
|
||||
SERVER_DIR="$MCC_SERVERS/$VERSION"
|
||||
if [[ -f "$SERVER_DIR/server.properties" ]]; then
|
||||
sed -i 's/^online-mode=.*/online-mode=false/' "$SERVER_DIR/server.properties"
|
||||
grep -q "^enable-rcon=" "$SERVER_DIR/server.properties" || echo "enable-rcon=true" >> "$SERVER_DIR/server.properties"
|
||||
grep -q "^rcon.password=" "$SERVER_DIR/server.properties" || echo "rcon.password=test123" >> "$SERVER_DIR/server.properties"
|
||||
grep -q "^rcon.port=" "$SERVER_DIR/server.properties" || echo "rcon.port=25575" >> "$SERVER_DIR/server.properties"
|
||||
fi
|
||||
bash "$ENSURE_SERVER_SCRIPT" "$VERSION" >/dev/null
|
||||
mc-start "$VERSION" >/dev/null
|
||||
|
||||
echo -n " Waiting for server..."
|
||||
|
|
@ -125,6 +126,10 @@ else
|
|||
done
|
||||
fi
|
||||
|
||||
if ! $PORT_SET_BY_USER; then
|
||||
PORT="$(bash "$GET_PORT_SCRIPT" "$VERSION")"
|
||||
fi
|
||||
|
||||
# --- Launch MCC ---
|
||||
echo "[4/4] Launching MCC in $MODE mode..."
|
||||
: > "$INPUT_FILE"
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ mc-cmd() { local v="${2:-1.20.6}"; echo "$1" > "$MCC_SERVERS/$v/stdin.pipe"; }
|
|||
mc-log() { local s; s=$(_mc-session "${1:-1.20.6}"); tmux capture-pane -t "$s" -p -S "-${2:-50}"; }
|
||||
mc-kill() { local v="${1:-1.20.6}" s; s=$(_mc-session "$v"); tmux kill-session -t "$s" 2>/dev/null; rm -f "$MCC_SERVERS/$v/stdin.pipe"; echo "Killed $s"; }
|
||||
mc-list() { tmux list-sessions 2>/dev/null | grep "^mc-" || echo "No running MC servers"; }
|
||||
mc-wait-ready() { bash "$MCC_REPO/.skills/mcc-integration-testing/scripts/preflight_test_env.sh" "${1:-1.20.6}" >/dev/null && source "$MCC_REPO/.skills/mcc-integration-testing/scripts/common.sh" && wait_for_server_ready "${1:-1.20.6}" "${2:-60}"; }
|
||||
mc-wait-stop() { source "$MCC_REPO/.skills/mcc-integration-testing/scripts/common.sh" && wait_for_server_stop "${1:-1.20.6}" "${2:-60}"; }
|
||||
mc-reset-test-env() { bash "$MCC_REPO/.skills/mcc-integration-testing/scripts/reset_shared_test_state.sh" "$@"; }
|
||||
|
||||
# --- RCON ---
|
||||
mc-rcon() { bash "$MCC_REPO/tools/mc-rcon.sh" "$@"; }
|
||||
|
|
@ -59,3 +62,4 @@ mcc-tui() {
|
|||
mcc-debug() { bash "$MCC_REPO/tools/mcc-debug.sh" "$@"; }
|
||||
mcc-log-mcc() { tail -f "${TMPDIR:-/tmp}/mcc-debug/mcc-debug.log" 2>/dev/null || echo "No MCC log found"; }
|
||||
mcc-state() { echo "debug state" >> "$MCC_REPO/mcc_input.txt"; sleep 1; tail -30 "${TMPDIR:-/tmp}/mcc-debug/mcc-debug.log" 2>/dev/null; }
|
||||
mcc-preflight() { bash "$MCC_REPO/.skills/mcc-integration-testing/scripts/preflight_test_env.sh" "$@"; }
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
# shellcheck source=tools/mcc-env.sh
|
||||
source "$REPO_ROOT/tools/mcc-env.sh"
|
||||
# shellcheck source=.skills/mcc-integration-testing/scripts/common.sh
|
||||
source "$REPO_ROOT/.skills/mcc-integration-testing/scripts/common.sh"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
|
|
@ -38,6 +40,7 @@ MCC_LOG="$TEST_ROOT/mcc.log"
|
|||
SERVER_LOG_FILE="$MCC_SERVERS/$SERVER_DIR/logs/latest.log"
|
||||
INPUT_FILE="$REPO_ROOT/mcc_input.txt"
|
||||
MCC_PID=""
|
||||
SERVER_PORT="25565"
|
||||
|
||||
mkdir -p "$TEST_ROOT"
|
||||
|
||||
|
|
@ -60,22 +63,6 @@ wait_for_file_pattern() {
|
|||
return 1
|
||||
}
|
||||
|
||||
wait_for_server_ready() {
|
||||
local timeout="${1:-60}"
|
||||
local elapsed=0
|
||||
|
||||
while (( elapsed < timeout )); do
|
||||
if mc-log "$SERVER_DIR" 250 2>/dev/null | grep -Fq "Done ("; then
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
((elapsed += 1))
|
||||
done
|
||||
|
||||
echo "Timed out waiting for server readiness" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
wait_for_rcon_port_free() {
|
||||
local timeout="${1:-30}"
|
||||
local elapsed=0
|
||||
|
|
@ -91,18 +78,6 @@ wait_for_rcon_port_free() {
|
|||
echo "Timed out waiting for RCON port 25575 to become free" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
kill_other_servers() {
|
||||
local sessions
|
||||
sessions="$(tmux list-sessions 2>/dev/null | awk -F: '/^mc-/{print $1}' || true)"
|
||||
if [[ -n "$sessions" ]]; then
|
||||
while IFS= read -r session; do
|
||||
[[ -z "$session" ]] && continue
|
||||
tmux kill-session -t "$session" 2>/dev/null || true
|
||||
done <<< "$sessions"
|
||||
fi
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
if [[ -n "${MCC_PID:-}" ]] && kill -0 "$MCC_PID" 2>/dev/null; then
|
||||
echo "quit" >> "$INPUT_FILE" 2>/dev/null || true
|
||||
|
|
@ -113,7 +88,7 @@ cleanup() {
|
|||
|
||||
if [[ -p "$MCC_SERVERS/$SERVER_DIR/stdin.pipe" ]]; then
|
||||
echo "stop" > "$MCC_SERVERS/$SERVER_DIR/stdin.pipe" 2>/dev/null || true
|
||||
sleep 2
|
||||
wait_for_server_stop "$SERVER_DIR" 20 >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
tmux kill-session -t "$SESSION_NAME" 2>/dev/null || true
|
||||
|
|
@ -127,21 +102,14 @@ prepare_config() {
|
|||
bash "$REPO_ROOT/.skills/mcc-integration-testing/scripts/prepare_offline_mcc_config.sh" \
|
||||
"$REPO_ROOT/MinecraftClient.ini" "$CFG" "$MC_VERSION" CursorBot >/dev/null
|
||||
|
||||
sed -i \
|
||||
sed_in_place \
|
||||
-e "s#^Server = .*#Server = { Host = \"localhost\", Port = $SERVER_PORT }#" \
|
||||
-e 's/TerrainAndMovements = false/TerrainAndMovements = true/' \
|
||||
-e 's/InventoryHandling = false/InventoryHandling = true/' \
|
||||
-e 's/EntityHandling = false/EntityHandling = true/' \
|
||||
-e 's/AutoRespawn = false/AutoRespawn = true/' \
|
||||
"$CFG"
|
||||
|
||||
sed -i '/^\[ChatBot.ScriptScheduler\]/,/^\[/ { s/^Enabled = true/Enabled = false/; }' "$CFG"
|
||||
sed -i '/^\[ChatBot.DiscordRpc\]/,/^\[/ { s/^Enabled = true/Enabled = false/; }' "$CFG"
|
||||
sed -i '/^\[ChatBot.AntiAFK\]/,/^\[/ { s/^Enabled = true/Enabled = false/; }' "$CFG"
|
||||
sed -i '/^\[ChatBot.AutoDig\]/,/^\[/ { s/^Enabled = true/Enabled = false/; }' "$CFG"
|
||||
sed -i '/^\[ChatBot.AutoAttack\]/,/^\[/ { s/^Enabled = true/Enabled = false/; }' "$CFG"
|
||||
sed -i '/^\[ChatBot.PlayerListLogger\]/,/^\[/ { s/^Enabled = true/Enabled = false/; }' "$CFG"
|
||||
sed -i '/^\[ChatBot.ReplayCapture\]/,/^\[/ { s/^Enabled = true/Enabled = false/; }' "$CFG"
|
||||
disable_noisy_bots_in_ini "$CFG"
|
||||
}
|
||||
|
||||
send_mcc_command() {
|
||||
|
|
@ -214,18 +182,19 @@ modern_mob_and_effects() {
|
|||
run_server_command "effect give CursorBot minecraft:regeneration 10 1 true"
|
||||
}
|
||||
|
||||
kill_other_servers
|
||||
bash "$REPO_ROOT/.skills/mcc-integration-testing/scripts/preflight_test_env.sh" "$SERVER_DIR" >/dev/null
|
||||
bash "$REPO_ROOT/.skills/mcc-integration-testing/scripts/reset_shared_test_state.sh" --all >/dev/null
|
||||
wait_for_rcon_port_free 30 || true
|
||||
rm -f "$MCC_LOG" "$INPUT_FILE"
|
||||
|
||||
bash "$REPO_ROOT/.skills/mcc-integration-testing/scripts/ensure_offline_server.sh" "$SERVER_DIR" >/dev/null
|
||||
SERVER_PORT="$(bash "$REPO_ROOT/.skills/mcc-integration-testing/scripts/get_server_port.sh" "$SERVER_DIR")"
|
||||
if [[ -f "$MCC_SERVERS/$SERVER_DIR/server.properties" ]]; then
|
||||
sed -i 's/^use-native-transport=.*/use-native-transport=false/' "$MCC_SERVERS/$SERVER_DIR/server.properties"
|
||||
sed_in_place 's/^use-native-transport=.*/use-native-transport=false/' "$MCC_SERVERS/$SERVER_DIR/server.properties"
|
||||
fi
|
||||
|
||||
mc-start "$SERVER_DIR" >/dev/null
|
||||
wait_for_server_ready || exit 1
|
||||
SERVER_PORT="$(bash "$REPO_ROOT/.skills/mcc-integration-testing/scripts/get_server_port.sh" "$SERVER_DIR")"
|
||||
wait_for_server_ready "$SERVER_DIR" || exit 1
|
||||
prepare_config
|
||||
|
||||
: > "$INPUT_FILE"
|
||||
|
|
@ -233,7 +202,11 @@ prepare_config
|
|||
(
|
||||
cd "$REPO_ROOT"
|
||||
MCC_FILE_INPUT=1 dotnet run --project MinecraftClient -c Release --no-build -- \
|
||||
"$CFG" CursorBot - "localhost:$SERVER_PORT" > "$MCC_LOG" 2>&1
|
||||
"$CFG" \
|
||||
CursorBot \
|
||||
- \
|
||||
"localhost:$SERVER_PORT" \
|
||||
> "$MCC_LOG" 2>&1
|
||||
) &
|
||||
MCC_PID=$!
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,38 @@
|
|||
#!/bin/bash
|
||||
# Start a Minecraft server in a tmux session with named pipe for stdin
|
||||
# Servers live under $MCC_SERVERS or default to MinecraftOfficial/downloads/<version>/.
|
||||
resolve_java_bin() {
|
||||
if command -v java >/dev/null 2>&1 && java -version >/dev/null 2>&1; then
|
||||
command -v java
|
||||
return 0
|
||||
fi
|
||||
|
||||
local candidate
|
||||
for candidate in \
|
||||
"${JAVA_BIN:-}" \
|
||||
"/opt/homebrew/opt/openjdk/bin/java" \
|
||||
"/usr/local/opt/openjdk/bin/java" \
|
||||
"/usr/lib/jvm/default-java/bin/java"
|
||||
do
|
||||
[[ -z "$candidate" ]] && continue
|
||||
if [[ -x "$candidate" ]]; then
|
||||
if "$candidate" -version >/dev/null 2>&1; then
|
||||
printf '%s\n' "$candidate"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
VERSION="${1}"
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
DOWNLOADS="${MCC_SERVERS:-$REPO_ROOT/MinecraftOfficial/downloads}"
|
||||
DIR="$DOWNLOADS/$VERSION"
|
||||
PIPE="$DIR/stdin.pipe"
|
||||
SESSION="mc-${VERSION//\./_}"
|
||||
JAVA_BIN="$(resolve_java_bin || true)"
|
||||
|
||||
if [ -z "$VERSION" ] || [ ! -d "$DIR" ]; then
|
||||
echo "Error: Server directory not found${VERSION:+: $DIR}"
|
||||
|
|
@ -20,6 +46,16 @@ if [ ! -f "$DIR/server.jar" ]; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v tmux >/dev/null 2>&1; then
|
||||
echo "Error: tmux is required to start local test servers"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "$JAVA_BIN" ]]; then
|
||||
echo "Error: Java was not found on PATH. Install Java or set JAVA_BIN." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if tmux has-session -t "$SESSION" 2>/dev/null; then
|
||||
echo "Server $VERSION already running in tmux session '$SESSION'"
|
||||
echo "View output: tmux capture-pane -t '$SESSION' -p -S -50"
|
||||
|
|
@ -29,10 +65,14 @@ fi
|
|||
|
||||
rm -f "$DIR/world/session.lock"
|
||||
|
||||
if [[ -e "$PIPE" && ! -p "$PIPE" ]]; then
|
||||
rm -f "$PIPE"
|
||||
fi
|
||||
|
||||
[ -p "$PIPE" ] || mkfifo "$PIPE"
|
||||
|
||||
tmux new-session -d -s "$SESSION" -c "$DIR" \
|
||||
"tail -f $PIPE | java -Xmx2G -Xms2G -jar server.jar nogui 2>&1"
|
||||
"tail -f $PIPE | '$JAVA_BIN' -Xmx2G -Xms2G -jar server.jar nogui 2>&1"
|
||||
|
||||
echo "Server $VERSION started in tmux session '$SESSION'"
|
||||
echo "Send commands: echo 'say hello' > $PIPE"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue