Compare commits

..

No commits in common. "feat/graph-edition" and "main" have entirely different histories.

20 changed files with 115 additions and 1849 deletions

View File

@ -1,16 +0,0 @@
{
"minecraft:seagrass": [97, 113, 54],
"minecraft:chest": [171, 121, 45],
"minecraft:ender_chest": [48, 67, 71],
"minecraft:lily_pad": [32, 128, 48],
"minecraft:redstone_wire": [189, 32, 8],
"minecraft:cocoa": [150, 87, 26],
"minecraft:wheat": [220, 187, 101],
"minecraft:carrots": [227, 138, 29],
"minecraft:potatoes": [200, 162, 75],
"minecraft:beetroots": [191, 37,41],
"minecraft:nether_wart": [131, 28, 32],
"minecraft:sweet_berry_bush": [60, 110, 66],
"minecraft:torchflower_crop": [130, 158, 85],
"minecraft:pitcher_crop": [112, 134, 181]
}

262
save.txt
View File

@ -1,262 +0,0 @@
n -220 -374 3 Port Pickle
n -240 -519 3 Pickledill
n -266 -454 4 Pickledill Valley
n -125 -518 2 Pickledill Waterfall
n -224 -562 1 Yeti Cave
n -225 -352 1 Port Pickle Observatory
n -227 -364 1 Port Pickle Smithery
n -212 -372 1 Pickledill Ferry Port Pickle
n -212 -97 1 Pickledill Ferry Shopping District
n -70 -67 1 Witch Farm
n -70 -77 0
n -79 -77 0
n -86 -75 0
n -92 -71 0
n -108 -61 0
n -127 -55 0
n -151 -52 0
n -168 -55 0
n -188 -61 0
n -198 -71 0
n -208 -84 0
n -213 -376 0
n -208 -372 0
n -200 -371 0
n -193 -373 0
n -216 -379 0
n -222 -379 0
n -226 -372 0
n -222 -369 0
n -222 -364 0
n -221 -359 0
n -219 -353 0
n -220 -349 0
n -225 -348 0
n -257 -479 1 Pickledill Mill
n -383 -499 1 Pickledill Pass
n -398 -517 1 Pickledill Bridge
n -279 -524 1 Pickledill Portal
n -407 -17 1 Portail du Shopping District
n -390 -22 4 Shopping District
n -480 -56 3 Shopping District Ouest
n -396 -4 1 Slime Shop
n -387 -3 1 Slime Shop
n -383 -13 1 Sand Shop
n -394 -29 1 Magasin de Livres
n -407 -37 1 Magasin de la Mer
n -370 -10 1 Shop de Fer
n -362 -17 1 Living Is A Right
n -399 -17 3 Shopping District Centre
n -373 -77 1 Magasin de Glace
n -315 -91 1 Puits du Shopping District
n -335 -43 1 Nether Goods
n -680 -199 4 Lycapark
n -392 -10 0
n -397 -12 0
n -396 -5 0
n -401 -26 0
n -406 -33 0
n -412 -35 0
n -396 -23 0
n -392 -18 0
n -385 -17 0
n -375 -16 0
n -376 -6 0
n -374 -2 0
n -373 2 0
n -368 6 0
n -370 -16 0
n -367 -21 0
n -361 -24 0
n -354 -24 0
n -352 -31 0
n -348 -39 0
n -343 -44 0
n -350 -19 0
n -348 -14 0
n -341 -13 0
n -339 -17 0
n -336 -22 0
n -341 -25 0
n -345 -29 0
n -331 -16 0
n -328 -11 0
n -324 -7 0
n -320 -3 0
n -317 2 0
n -315 7 0
n -310 6 0
n -306 4 0
n -299 3 0
n -295 -4 0
n -296 -14 0
n -299 -23 0
n -304 -31 0
n -311 -28 0
n -319 -24 0
n -328 -22 0
n -299 -37 0
n -297 -41 0
n -297 -51 0
n -291 -61 0
n -296 -68 0
n -300 -64 0
n -304 -60 0
n -307 -58 0
n -315 -57 0
n -320 -53 0
n -324 -52 0
n -330 -53 0
n -337 -53 0
n -291 -72 0
n -286 -73 0
n -282 -76 0
n -279 -77 0
n -273 -83 0
n -269 -90 0
n -260 -92 0
n -253 -96 0
n -239 -96 0
n -231 -94 0
n -223 -97 0
n -419 -32 0
n -423 -29 0
n -428 -28 0
n -434 -26 0
n -435 -22 0
n -442 -17 0
n -446 -19 0
n -452 -21 0
n -457 -24 0
n -462 -26 0
n -465 -31 0
n -469 -37 0
n -471 -44 0
n -475 -51 0
n -286 -91 3 Shopping District Est
e 7 8 2
e 10 9 0
e 10 11 3
e 11 12 3
e 12 13 3
e 13 14 3
e 14 15 3
e 15 16 3
e 16 17 3
e 17 18 3
e 18 19 3
e 19 20 3
e 20 8 3
e 7 21 0
e 21 22 0
e 22 23 0
e 23 24 0
e 21 25 0
e 25 26 0
e 26 27 0
e 27 28 0
e 28 29 0
e 29 6 0
e 29 30 0
e 30 31 0
e 31 32 0
e 32 33 0
e 33 5 0
e 42 53 0
e 53 54 0
e 54 55 0
e 54 48 0
e 48 38 0
e 48 56 0
e 56 57 0
e 57 58 0
e 57 45 0
e 45 48 0
e 48 59 0
e 59 44 0
e 48 60 0
e 60 61 0
e 61 43 0
e 61 62 0
e 63 64 0
e 64 65 0
e 65 66 0
e 63 62 0
e 62 67 0
e 67 46 0
e 67 47 0
e 67 68 0
e 68 69 0
e 69 70 0
e 70 71 0
e 71 72 0
e 72 73 0
e 73 51 0
e 70 74 0
e 74 75 0
e 75 76 0
e 76 77 0
e 77 78 0
e 78 79 0
e 79 80 0
e 80 71 0
e 77 81 0
e 81 82 0
e 82 83 0
e 83 84 0
e 84 85 0
e 85 86 0
e 86 87 0
e 87 88 0
e 88 89 0
e 89 90 0
e 90 91 0
e 91 92 0
e 92 93 0
e 93 94 0
e 94 95 0
e 95 96 0
e 96 78 0
e 93 97 0
e 97 98 0
e 98 99 0
e 99 100 0
e 100 101 0
e 101 102 0
e 102 103 0
e 103 104 0
e 104 105 0
e 105 106 0
e 106 107 0
e 107 108 0
e 108 109 0
e 109 73 0
e 101 110 0
e 110 111 0
e 111 112 0
e 112 113 0
e 113 114 0
e 114 115 0
e 115 116 0
e 116 117 0
e 117 118 0
e 118 119 0
e 119 120 0
e 120 8 0
e 58 121 0
e 121 122 0
e 122 123 0
e 123 124 0
e 124 125 0
e 125 126 0
e 126 127 0
e 127 128 0
e 128 129 0
e 129 130 0
e 130 131 0
e 131 132 0
e 132 133 0
e 133 134 0
e 134 40 0
e 101 112 0

View File

@ -1,28 +0,0 @@
import json
import os.path
class Config:
LAST_OPENED_FILE = ""
AUTOSAVE_INTERVAL = 5 * 60 * 1000
CACHE_TTL = 10
def __init__(self, path: str):
self._path: str = path
self.load()
def load(self) -> None:
if os.path.exists(self._path):
with open(self._path, "r") as f:
config = json.load(f)
self.LAST_OPENED_FILE = config["last_opened_file"]
self.AUTOSAVE_INTERVAL = config["autosave_interval"]
def save(self) -> None:
with open(self._path, "w") as f:
json.dump({
"last_opened_file": self.LAST_OPENED_FILE,
"autosave_interval": self.AUTOSAVE_INTERVAL
}, f, indent=4)

View File

@ -3,191 +3,77 @@ from enum import Enum, auto
from math import floor
from typing import Optional
import platformdirs
import pygame
from src.config import Config
from src.graph.graph import Graph
from src.image_handler import ImageHandler
from src.utils.paths import CONFIG_DIR, CACHE_DIR
class Editor:
APP_NAME: str = "lycacraft-paths"
APP_AUTHOR: str = "Lycacraft"
WIDTH: int = 800
HEIGHT: int = 600
MAP_SIZE: int = 1024
CONFIG_PATH: str = os.path.join(CONFIG_DIR, "config.json")
MAPS_DIR: str = os.path.join(CACHE_DIR, "maps")
AUTOSAVE_PATH: str = os.path.join(CACHE_DIR, "AUTOSAVE.txt")
AUTOSAVE_EVENT: int = pygame.event.custom_type()
ZOOMS: tuple[float] = tuple(2**p for p in range(-6, 7))
MAPS_DIR: str = os.path.join(platformdirs.user_cache_dir(appname="lycacraft-paths", appauthor="Lycacraft"), "maps")
ZOOMS: tuple[float] = (0.25, 0.5, 1, 2, 4)
CROSSHAIR_SIZE: int = 10
EDGE_TYPE_KEYS: dict[int, str] = {
pygame.K_p: "path",
pygame.K_n: "narrow_path",
pygame.K_f: "ferry",
pygame.K_b: "boat",
pygame.K_r: "rails"
}
NODE_TYPE_KEYS: list[int] = [
pygame.K_0,
pygame.K_1,
pygame.K_2,
pygame.K_3,
pygame.K_4
]
def __init__(self):
pygame.init()
self.config: Config = Config(self.CONFIG_PATH)
self.width: int = self.WIDTH
self.height: int = self.HEIGHT
self.win: pygame.Surface = pygame.display.set_mode([self.width, self.height], pygame.RESIZABLE)
pygame.display.set_caption("Lycacraft Map Editor")
self.center: list[int] = [0, 0]
self.zoom_i: int = self.ZOOMS.index(1)
self.zoom_i: int = 2
self.zoom: float = self.ZOOMS[self.zoom_i]
self.running: bool = False
self.image_handler: ImageHandler = ImageHandler(self.MAPS_DIR, self.MAP_SIZE, self.config.CACHE_TTL)
self.image_handler: ImageHandler = ImageHandler(self.MAPS_DIR, self.MAP_SIZE)
self.clock: pygame.time.Clock = pygame.time.Clock()
self.left_drag_pos: Optional[tuple[int, int]] = None
self.mid_drag_pos: Optional[tuple[int, int]] = None
self.font: pygame.font.Font = pygame.font.SysFont("Ubuntu", 20)
self.loading_font: pygame.font.Font = pygame.font.SysFont("Ubuntu", 30)
self.zooms_texts: list[pygame.Surface] = []
for zoom in self.ZOOMS:
txt = str(zoom)
if zoom < 1:
txt = f"1/{int(1/zoom):d}"
self.zooms_texts.append(self.font.render(txt, True, (255, 255, 255)))
self.is_renaming_node: bool = False
self.zooms_texts: list[pygame.Surface] = list(map(
lambda z: self.font.render(str(z), True, (255, 255, 255)),
self.ZOOMS
))
self.state: State = State.STOPPING
self.graph = Graph()
self.typing_text: str = ""
self.node_radius: int = 5
self.line_size: int = 3
self.edge_detect_radius: int = 3 * self.line_size
self.selected_nodes: list[int] = []
self.selected_edges: list[int] = []
self.previously_created_nodes: list[int] = []
self.selection_rectangle: Optional[list[tuple[int, int], tuple[int, int]]] = None
self.original_move_pos: Optional[tuple[int, int]] = None
self.move_old_poses: Optional[dict[int, tuple[int, int]]] = None
self.dirty: bool = False
self.loading_bg: pygame.Surface = pygame.Surface([self.width, self.height])
pygame.time.set_timer(self.AUTOSAVE_EVENT, self.config.AUTOSAVE_INTERVAL)
if os.path.exists(self.AUTOSAVE_PATH):
self.load(self.AUTOSAVE_PATH, False)
self.dirty = True
elif self.config.LAST_OPENED_FILE != "":
self.load(self.config.LAST_OPENED_FILE)
# =========================
# EVENTS
# =========================
def mainloop(self) -> None:
self.state = State.LOADING
while self.state != State.STOPPING:
caption = f"Lycacraft Map Editor - {self.clock.get_fps():.2f}fps - {self.image_handler.size} images"
if self.dirty:
caption += " (unsaved)"
pygame.display.set_caption(caption)
pygame.display.set_caption(f"Lycacraft Map Editor - {self.clock.get_fps():.2f}fps")
self.process_events()
if self.state == State.LOADING:
self.render_loading()
if not self.image_handler.loading:
self.state = State.RUNNING
elif self.state == State.RUNNING:
if self.selection_rectangle is not None:
self.expand_selection_rect()
if self.original_move_pos is not None:
self.move_poses()
self.render()
self.image_handler.clean()
self.clock.tick(30)
def quit(self) -> None:
if self.dirty:
self.save(self.AUTOSAVE_PATH, False)
self.state = State.STOPPING
def process_events(self) -> None:
events = pygame.event.get()
keys = pygame.key.get_pressed()
for event in events:
if event.type == pygame.QUIT:
self.quit()
self.state = State.STOPPING
elif event.type == pygame.WINDOWRESIZED:
self.width = event.x
self.height = event.y
elif event.type == pygame.KEYDOWN:
if self.is_renaming_node:
if event.key == pygame.K_ESCAPE:
self.is_renaming_node = False
self.typing_text = ""
elif event.key == pygame.K_RETURN:
self.rename_nodes()
elif event.key == pygame.K_BACKSPACE:
self.typing_text = self.typing_text[:-1]
else:
self.typing_text += event.unicode
else:
if event.key == pygame.K_ESCAPE:
if self.selected_nodes != [] or self.selected_edges != []:
self.clear_selection()
self.previously_created_nodes = []
else:
self.quit()
self.state = State.STOPPING
elif event.key == pygame.K_PAGEUP:
self.zoom_in()
elif event.key == pygame.K_PAGEDOWN:
self.zoom_out()
elif event.key == pygame.K_BACKSPACE:
self.deleted_selected_objects()
elif event.key == pygame.K_s and event.mod & (pygame.KMOD_CTRL | pygame.KMOD_META):
self.save()
elif event.key == pygame.K_l and event.mod & (pygame.KMOD_CTRL | pygame.KMOD_META):
self.load()
elif event.key == pygame.K_RETURN:
if len(self.selected_nodes) > 0:
self.typing_text = ""
if len(self.selected_nodes) == 1:
self.typing_text = self.graph.nodes[self.selected_nodes[0]].name
self.is_renaming_node = True
elif event.key == pygame.K_HOME:
self.center = [0, 0]
elif event.key == pygame.K_F5:
self.reload()
elif event.key in self.EDGE_TYPE_KEYS.keys():
self.change_edge_types(self.EDGE_TYPE_KEYS[event.key])
elif event.key in self.NODE_TYPE_KEYS:
self.change_node_types(self.NODE_TYPE_KEYS.index(event.key))
elif event.type == pygame.KEYUP:
if event.key == pygame.K_m:
if self.original_move_pos is not None:
self.reset_move_poses()
elif event.type == pygame.MOUSEBUTTONDOWN:
if event.button == 2:
self.mid_drag_pos = event.pos
elif event.button == 1:
if keys[pygame.K_LCTRL]:
elif event.button == 1 and keys[pygame.K_LCTRL]:
self.left_drag_pos = event.pos
elif keys[pygame.K_LALT]:
self.create_selection_rect(keys[pygame.K_LSHIFT] or keys[pygame.K_RSHIFT])
elif keys[pygame.K_m]:
self.original_move_pos = event.pos
self.start_moving()
else:
self.select_object(keys[pygame.K_LSHIFT] or keys[pygame.K_RSHIFT])
elif event.button == 3:
self.create_node(self.screen_to_world(event.pos[0], event.pos[1]))
elif event.button == 4:
self.zoom_in()
elif event.button == 5:
@ -196,15 +82,7 @@ class Editor:
if event.button == 2:
self.mid_drag_pos = None
elif event.button == 1:
if keys[pygame.K_LCTRL]:
self.left_drag_pos = None
elif keys[pygame.K_m] and self.original_move_pos is not None:
self.confirm_move_poses()
elif self.selection_rectangle is not None:
self.release_selection_rect(keys[pygame.K_LSHIFT] or keys[pygame.K_RSHIFT])
elif event.type == self.AUTOSAVE_EVENT:
if self.dirty:
self.save(self.AUTOSAVE_PATH, False)
if keys[pygame.K_LEFT]:
self.center[0] -= 4 / self.zoom
@ -228,13 +106,8 @@ class Editor:
if mbtns[1]:
self.mid_drag_pos = mpos
# =========================
# RENDERING
# =========================
def render(self) -> None:
self.win.fill((50, 50, 50))
self.win.fill((0, 0, 0))
off_x = (self.center[0] * self.zoom) % self.MAP_SIZE
off_y = (self.center[1] * self.zoom) % self.MAP_SIZE
@ -270,10 +143,6 @@ class Editor:
oy + y * self.MAP_SIZE
])
self.render_graph()
self.render_selection_rect()
pygame.draw.line(self.win, (150, 150, 150), [w2 - self.CROSSHAIR_SIZE, h2], [w2 + self.CROSSHAIR_SIZE, h2])
pygame.draw.line(self.win, (150, 150, 150), [w2, h2 - self.CROSSHAIR_SIZE], [w2, h2 + self.CROSSHAIR_SIZE])
self.render_zoom_slider()
@ -284,19 +153,16 @@ class Editor:
pygame.draw.rect(self.win, (80, 80, 80), [0, 0, mouse_txt.get_width() + 10, mouse_txt.get_height() + 10])
self.win.blit(mouse_txt, [5, 5])
if self.is_renaming_node:
self.render_node_renamer()
pygame.display.flip()
def render_zoom_slider(self) -> None:
zoom_r = self.height / 80
zoom_space = zoom_r * 4
zoom_height = zoom_space * (len(self.ZOOMS) - 1)
zoom_height = self.height * 0.2
zoom_h_margin = self.width * 0.02
zoom_v_margin = self.height * 0.05
zoom_x = self.width - zoom_h_margin
zoom_y = self.height - zoom_v_margin - zoom_height
zoom_space = zoom_height / 4
zoom_r = zoom_space / 4
zoom_width = max(s.get_width() for s in self.zooms_texts) + 2 * zoom_r + 5
pygame.draw.rect(self.win, (80, 80, 80), [
zoom_x + zoom_r - zoom_width - 5,
@ -313,7 +179,6 @@ class Editor:
def render_loading(self) -> None:
self.win.fill((0, 0, 0))
self.win.blit(self.loading_bg, [0, 0])
count = self.image_handler.count
total = self.image_handler.total
txt = self.loading_font.render(f"Loading maps - {count}/{total}", True, (255, 255, 255))
@ -324,82 +189,12 @@ class Editor:
h2 = self.height / 2
x0 = w2 - width / 2
y0 = h2 - height / 2
loaded_width = 0 if total == 0 else width * count / total
pygame.draw.rect(self.win, (160, 160, 160), [x0, y0, width, height])
pygame.draw.rect(self.win, (90, 250, 90), [x0, y0, loaded_width, height])
pygame.draw.rect(self.win, (90, 250, 90), [x0, y0, width * count / total, height])
self.win.blit(txt, [w2 - txt.get_width() / 2, y0 - txt.get_height() - 5])
pygame.display.flip()
def render_node_renamer(self) -> None:
width = self.width / 2
height = self.height / 2
x0 = (self.width - width) / 2
y0 = (self.height - height) / 2
line_height = height / 6
nc_txt = self.loading_font.render("RENAME NODE", True, (255, 255, 255))
name_txt = self.loading_font.render("New name:", True, (255, 255, 255))
txt = self.loading_font.render(self.typing_text, True, (255, 255, 255))
pygame.draw.rect(self.win, (0, 0, 0), [x0, y0, width, height])
self.win.blit(nc_txt, [self.width / 2 - nc_txt.get_width() / 2, y0 + line_height])
self.win.blit(name_txt, [self.width / 2 - name_txt.get_width() / 2, y0 + 3 * line_height])
self.win.blit(txt, [self.width / 2 - txt.get_width() / 2, y0 + 4 * line_height])
def render_graph(self) -> None:
self.render_edges()
self.render_nodes()
def render_edges(self) -> None:
hover_index, is_node = self.get_hover_object()
for edge in self.graph.edges:
node_1, node_2 = self.graph.get_edge_nodes(edge)
color = self.graph.TYPE_COLORS[edge.type]
start = self.world_to_screen(node_1.x, node_1.z)
end = self.world_to_screen(node_2.x, node_2.z)
if not is_node and edge.index == hover_index:
pygame.draw.line(self.win, (0, 0, 0), start, end, self.edge_detect_radius)
elif edge.index in self.selected_edges:
pygame.draw.line(self.win, (255, 255, 255), start, end, self.edge_detect_radius)
pygame.draw.line(self.win, color, start, end, self.line_size)
def render_nodes(self) -> None:
hover_index, is_node = self.get_hover_object()
for node in self.graph.nodes:
blitpos = self.world_to_screen(node.x, node.z)
color = self.graph.TYPE_COLORS[node.type]
if is_node and node.index == hover_index:
self.render_hover_node(node.index)
elif node.index in self.selected_nodes:
pygame.draw.circle(self.win, (255, 255, 255), (blitpos[0], blitpos[1]), self.node_radius + self.line_size)
pygame.draw.circle(self.win, color, (blitpos[0], blitpos[1]), self.node_radius)
def render_hover_node(self, node_index: int) -> None:
node = self.graph.nodes[node_index]
txt = self.loading_font.render(node.name, True, (255, 255, 255))
node_pos = self.world_to_screen(node.x, node.z)
xpos = node_pos[0] - txt.get_width() - self.node_radius * (2 ** -0.5)
ypos = node_pos[1] - txt.get_height() - self.node_radius * (2 ** -0.5)
pygame.draw.rect(self.win, (0, 0, 0), pygame.Rect(xpos, ypos, txt.get_width(), txt.get_height()))
self.win.blit(txt, [xpos, ypos])
pygame.draw.circle(self.win, (0, 0, 0), (node_pos[0], node_pos[1]), self.node_radius + self.line_size)
def render_selection_rect(self) -> None:
rect = self.selection_rectangle
if rect is not None:
left = min(rect[0][0], rect[1][0])
top = min(rect[0][1], rect[1][1])
width = abs(rect[0][0] - rect[1][0])
height = abs(rect[0][1] - rect[1][1])
pygame.draw.rect(self.win, (32, 32, 32), pygame.Rect(left, top, width, height), self.line_size)
# =========================
# ZOOMING
# =========================
def set_zoom(self, zoom_i: int) -> None:
self.zoom_i = max(0, min(len(self.ZOOMS) - 1, zoom_i))
self.zoom = self.ZOOMS[self.zoom_i]
@ -410,259 +205,6 @@ class Editor:
def zoom_out(self) -> None:
self.set_zoom(self.zoom_i - 1)
# =========================
# SELECTION
# =========================
def select_object(self, shifting: bool = False) -> None:
hover_index, is_node = self.get_hover_object()
self.previously_created_nodes = []
if is_node:
self.select_node(hover_index, shifting)
elif hover_index != -1:
self.select_edge(hover_index, shifting)
else:
self.clear_selection()
def select_node(self, node: int, shifting: bool = False) -> None:
if shifting:
if node in self.selected_nodes:
self.selected_nodes.remove(node)
return
if node != -1:
self.selected_nodes.append(node)
return
if node in self.selected_nodes:
self.clear_selection()
self.selected_nodes.append(node)
return
if node != -1:
for sel_node in self.selected_nodes:
self.link_nodes(sel_node, node)
self.selected_nodes = [] if node == -1 else [node]
self.selected_edges = []
def select_edge(self, edge: int, shifting: bool = False) -> None:
if shifting:
if edge in self.selected_edges:
self.selected_edges.remove(edge)
return
if edge != -1:
self.selected_edges.append(edge)
return
if edge in self.selected_edges:
self.clear_selection()
self.selected_edges.append(edge)
return
self.selected_edges = [] if edge == -1 else [edge]
self.selected_nodes = []
def clear_selection(self) -> None:
self.selected_nodes = []
self.selected_edges = []
def clear_node_selection(self) -> None:
self.selected_nodes = []
def clear_edge_selection(self) -> None:
self.selected_edges = []
def create_selection_rect(self, shifting: bool = False):
if not shifting:
self.clear_selection()
self.previously_created_nodes = []
mouse_pos = pygame.mouse.get_pos()
self.selection_rectangle = [mouse_pos, mouse_pos]
def expand_selection_rect(self):
self.selection_rectangle[1] = pygame.mouse.get_pos()
def release_selection_rect(self, shifting: bool = False):
if not shifting:
self.clear_selection()
self.previously_created_nodes = []
rect = self.selection_rectangle
left = min(rect[0][0], rect[1][0])
top = min(rect[0][1], rect[1][1])
right = max(rect[0][0], rect[1][0])
bottom = max(rect[0][1], rect[1][1])
for node in self.graph.nodes:
pos = self.world_to_screen(node.x, node.z)
if left <= pos[0] <= right and top <= pos[1] <= bottom:
if node.index not in self.selected_nodes:
self.selected_nodes.append(node.index)
for edge in self.graph.edges:
pos = self.world_to_screen(*self.graph.get_edge_center(edge.index))
if left <= pos[0] <= right and top <= pos[1] <= bottom:
if edge.index not in self.selected_edges:
self.selected_edges.append(edge.index)
self.selection_rectangle = None
# =========================
# HOVERING
# =========================
def get_hovering_nodes(self) -> tuple[list[int], list[float]]:
hovering = []
dists = []
mouse_pos = pygame.mouse.get_pos()
for node in self.graph.nodes:
dist = self.get_node_distance(node.index, mouse_pos[0], mouse_pos[1])
if dist < self.node_radius:
hovering.append(node.index)
dists.append(dist)
return hovering, dists
def get_hover_node(self) -> int:
hover_nodes, distances = self.get_hovering_nodes()
return -1 if len(hover_nodes) == 0 else hover_nodes[distances.index(min(distances))]
def get_hovering_edges(self) -> tuple[list[int], list[float]]:
hovering = []
dists = []
mouse_pos = pygame.mouse.get_pos()
for edge in self.graph.edges:
dist = self.get_edge_distance(edge.index, mouse_pos[0], mouse_pos[1])
if dist < self.edge_detect_radius:
hovering.append(edge.index)
dists.append(dist)
return hovering, dists
def get_hover_edge(self) -> int:
hover_edges, distances = self.get_hovering_edges()
return -1 if len(hover_edges) == 0 else hover_edges[distances.index(min(distances))]
def get_hover_object(self) -> tuple[int, bool]:
node = self.get_hover_node()
if node != -1:
return node, True
edge = self.get_hover_edge()
if edge != -1:
return edge, False
return -1, False
# =========================
# CREATION
# =========================
def link_nodes(self, node_1: int, node_2: int) -> None:
if not self.graph.edge_exists(node_1, node_2):
self.create_edge(node_1, node_2)
def create_node(self, pos: tuple[int, int], typing_text: str = "") -> None:
self.dirty = True
self.graph.add_node(pos[0], pos[1], typing_text, type=self.get_node_type())
if len(self.selected_nodes) == 1:
self.previously_created_nodes.append(self.selected_nodes[0])
self.select_node(self.graph.number_of_nodes() - 1)
def create_edge(self, node_1: int, node_2: int) -> None:
self.dirty = True
self.graph.add_edge(node_1, node_2, type=self.get_edge_type())
def get_edge_type(self) -> str:
type = ""
for key, value in self.EDGE_TYPE_KEYS.items():
if pygame.key.get_pressed()[key]:
type = value
break
return type
def get_node_type(self) -> int:
type = 0
for i in range(len(self.NODE_TYPE_KEYS)):
if pygame.key.get_pressed()[self.NODE_TYPE_KEYS[i]]:
type = i
break
return type
# =========================
# EDITION
# =========================
def rename_nodes(self) -> None:
self.dirty = True
for node in self.selected_nodes:
self.graph.nodes[node].rename_node(self.typing_text)
self.typing_text = ""
self.is_renaming_node = False
def start_moving(self):
self.move_old_poses = {}
for node_index in self.selected_nodes:
node = self.graph.nodes[node_index]
self.move_old_poses[node_index] = (node.x, node.z)
def move_poses(self):
mouse_pos = pygame.mouse.get_pos()
start_pos = self.original_move_pos
delta_x = mouse_pos[0] - start_pos[0]
delta_z = mouse_pos[1] - start_pos[1]
for node_index in self.move_old_poses.keys():
node = self.graph.nodes[node_index]
old_pos = self.move_old_poses[node_index]
node.x = old_pos[0] + delta_x
node.z = old_pos[1] + delta_z
def reset_move_poses(self):
self.original_move_pos = None
for node_index in self.move_old_poses.keys():
node = self.graph.nodes[node_index]
old_pos = self.move_old_poses[node_index]
node.x = old_pos[0]
node.z = old_pos[1]
self.move_old_poses = None
def confirm_move_poses(self):
self.dirty = True
self.original_move_pos = None
self.move_old_poses = None
def change_edge_types(self, type: str = "path"):
self.dirty = True
for edge in self.selected_edges:
self.graph.set_edge_type(edge, type)
def change_node_types(self, type: int = 0):
self.dirty = True
for node_index in self.selected_nodes:
node = self.graph.nodes[node_index]
node.set_type(type)
def deleted_selected_objects(self):
self.dirty = True
edges_to_delete = [self.graph.edges[i] for i in self.selected_edges]
nodes_to_delete = [self.graph.nodes[i] for i in self.selected_nodes]
for edge in edges_to_delete:
self.graph.delete_edge(edge)
for node in nodes_to_delete:
self.graph.delete_node(node)
self.clear_selection()
n = len(self.previously_created_nodes)
if n != 0:
self.selected_nodes.append(self.previously_created_nodes[n - 1])
self.previously_created_nodes.pop()
# =========================
# CALCULATION
# =========================
def screen_to_world(self, x: int, y: int) -> tuple[int, int]:
w2 = self.width / 2
h2 = self.height / 2
@ -671,81 +213,6 @@ class Editor:
return int(world_x), int(world_z)
def world_to_screen(self, world_x: int, world_z: int) -> tuple[int, int]:
w2 = self.width / 2
h2 = self.height / 2
screen_x = (world_x - self.center[0]) * self.zoom + w2
screen_y = (world_z - self.center[1]) * self.zoom + h2
return int(screen_x), int(screen_y)
def get_edge_distance(self, edge_i: int, px: int, pz: int) -> float:
start_n, end_n = self.graph.get_edge_nodes(self.graph.edges[edge_i])
start_p = self.world_to_screen(start_n.x, start_n.z)
end_p = self.world_to_screen(end_n.x, end_n.z)
edge_vec = (end_p[0] - start_p[0], end_p[1] - start_p[1])
start_vec = (px - start_p[0], pz - start_p[1])
edge_vec_len = (edge_vec[0] ** 2 + edge_vec[1] ** 2) ** 0.5
if edge_vec_len == 0:
return self.get_node_distance(start_n.index, px, pz)
scal_prod = start_vec[0] * edge_vec[0] + start_vec[1] * edge_vec[1]
proj_len = scal_prod / edge_vec_len
if proj_len < 0:
return self.get_node_distance(start_n.index, px, pz)
if proj_len > edge_vec_len:
return self.get_node_distance(end_n.index, px, pz)
return abs((edge_vec[0] * start_vec[1] - edge_vec[1] * start_vec[0]) / edge_vec_len)
def get_node_distance(self, node_i: int, px: int, pz: int) -> float:
node = self.graph.nodes[node_i]
node_pos = self.world_to_screen(node.x, node.z)
return ((px - node_pos[0]) ** 2 + (pz - node_pos[1]) ** 2) ** 0.5
# SAVING
def save(self, path: Optional[str] = None, save_config: bool = True) -> None:
last_path = self.config.LAST_OPENED_FILE
if path is None:
path = input(f"Save as ({last_path}): ")
if len(path.strip()) == 0:
path = last_path
self.graph.save(path)
if save_config:
self.config.LAST_OPENED_FILE = path
self.config.save()
if os.path.exists(self.AUTOSAVE_PATH):
os.remove(self.AUTOSAVE_PATH)
self.dirty = False
def load(self, path: Optional[str] = None, save_config: bool = True) -> None:
last_path = self.config.LAST_OPENED_FILE
if path is None:
path = input(f"Load from ({last_path}): ")
if len(path.strip()) == 0:
path = last_path
if os.path.exists(path):
self.graph = Graph.load(path)
if save_config:
self.config.LAST_OPENED_FILE = path
self.config.save()
self.dirty = False
def reload(self) -> None:
self.state = State.LOADING
self.loading_bg = self.win.copy()
del self.image_handler
self.image_handler = ImageHandler(self.MAPS_DIR, self.MAP_SIZE, self.config.CACHE_TTL)
class State(Enum):
STOPPING = auto()

View File

View File

@ -1,8 +0,0 @@
class Edge:
def __init__(self, start: int, end: int, length: float, index: int, type: int):
self.length: float = length
self.start: int = start
self.end: int = end
self.index: int = index
self.type: int = type

View File

@ -1,161 +0,0 @@
from __future__ import annotations
from math import inf, sqrt
from typing import Iterator, Optional
from src.graph.node import Node
from src.graph.edge import Edge
class Graph:
EDGE_TYPES: list[str] = [
"path",
"narrow_path",
"ferry",
"boat",
"rails"
]
TYPE_COLORS: list[tuple[int, int, int]] = [
(255, 0, 0),
(255, 0, 255),
(0, 255, 0),
(255, 255, 0),
(0, 0, 255)
]
def __init__(self):
self.edges: list[Edge] = []
self.nodes: list[Node] = []
def add_node(self, x: int, z: int, name: str = "", type: int = 0) -> None:
self.nodes.append(Node(x, z, len(self.nodes), name, type))
def add_edge(self, start_index: int, end_index: int, type: str = "path", auto_length: bool = True) -> None:
length = 0
if auto_length:
n1, n2 = self.nodes[start_index], self.nodes[end_index]
length = sqrt((n1.x - n2.x)**2 + (n1.z - n2.z)**2)
type_n = 0 if type not in self.EDGE_TYPES else self.EDGE_TYPES.index(type)
self.edges.append(Edge(start_index, end_index, length, len(self.edges), type_n))
def delete_edge(self, edge: Edge) -> None:
self.edges.remove(edge)
for ed in self.edges:
ed.index = self.edges.index(ed)
def delete_node(self, node: Node) -> None:
edges_to_delete = []
for edge in self.edges:
if node.index in (edge.start, edge.end):
edges_to_delete.append(edge)
continue
if edge.start > node.index:
edge.start -= 1
if edge.end > node.index:
edge.end -= 1
for edge in edges_to_delete:
self.delete_edge(edge)
self.nodes.remove(node)
for no in self.nodes:
no.index = self.nodes.index(no)
def recompute_lengths(self) -> None:
for edge in self.edges:
n1 = self.nodes[edge.start]
n2 = self.nodes[edge.end]
edge.length = sqrt((n1.x - n2.x)**2 + (n1.z - n2.z)**2)
def number_of_nodes(self) -> int:
return len(self.nodes)
def get_edge(self, node_1: int, node_2: int) -> int:
for edge in self.edges:
if (edge.start == node_1 and edge.end == node_2) or (edge.start == node_2 and edge.end == node_1):
return self.edges.index(edge)
return -1
def get_edge_nodes(self, edge: Edge) -> tuple[Node, Node]:
return self.nodes[edge.start], self.nodes[edge.end]
def get_edge_center(self, edge_index: int) -> tuple[float, float]:
edge = self.edges[edge_index]
start_n = self.nodes[edge.start]
end_n = self.nodes[edge.end]
return (start_n.x + end_n.x) / 2, (start_n.z + end_n.z) / 2
def edges_adjacent_to(self, node_i: int) -> Iterator[Edge]:
return filter(lambda e: e.start == node_i or e.end == node_i, self.edges)
def edge_exists(self, node_1: int, node_2: int) -> bool:
return self.get_edge(node_1, node_2) != -1
def set_edge_type(self, edge_index: int, type: str = "path") -> None:
edge = self.edges[edge_index]
edge.type = 0 if type not in self.EDGE_TYPES else self.EDGE_TYPES.index(type)
def dijkstra(self, source_index: int, target_index: int) -> Optional[list[int]]:
n = len(self.nodes)
if source_index < 0 or source_index >= n:
return None
if target_index < 0 or target_index >= n:
return None
unvisited = list(range(n))
distances_from_start = [inf] * n
distances_from_start[source_index] = 0
node_sequences = [[] for _ in range(n)]
node_sequences[source_index] = [source_index]
while True:
current_index = min(unvisited, key=lambda i: distances_from_start[i])
if current_index == target_index:
break
unvisited.remove(current_index)
for edge in self.edges_adjacent_to(current_index):
start = current_index
end = edge.end if edge.start == current_index else edge.start
if end in unvisited and distances_from_start[end] > distances_from_start[start] + edge.length:
distances_from_start[end] = distances_from_start[start] + edge.length
node_sequences[end] = node_sequences[start].copy()
node_sequences[end].append(end)
return node_sequences[target_index]
def save(self, path: str) -> None:
with open(path, "w") as f:
for node in self.nodes:
f.write(f"n {node.x} {node.z} {node.type} {node.name}\n")
f.write("\n")
for edge in self.edges:
f.write(f"e {edge.start} {edge.end} {edge.type}\n")
@staticmethod
def load(path: str) -> Graph:
graph = Graph()
with open(path, "r") as f:
lines = f.read().splitlines()
for line in lines:
if len(line.strip()) == 0:
continue
entry_type, values = line.split(" ", 1)
if entry_type == "n":
x, z, type, name = values.split(" ", 3)
x, z, type = int(x), int(z), int(type)
graph.add_node(x, z, name, type)
elif entry_type == "e":
start, end, type = values.split(" ", 2)
start, end, type = int(start), int(end), int(type)
graph.add_edge(start, end, auto_length=False, type=graph.EDGE_TYPES[type])
graph.recompute_lengths()
return graph

View File

@ -1,18 +0,0 @@
class Node:
def __init__(self, x: int, z: int, index: int, name: str = "", type: int = 0):
self.x: int = x
self.z: int = z
self.index: int = index
self.name: str = name
self.type: int = (0 if name == "" else type)
def rename_node(self, name: str) -> None:
if self.name == "" and name != "":
self.type = 1
self.name = name
if name == "":
self.type = 0
def set_type(self, type: int = 0) -> None:
if type != 0:
self.type = 0 if self.name == "" else type

View File

@ -1,6 +1,5 @@
import os
import threading
import time
from math import floor
from typing import Optional
@ -8,13 +7,10 @@ import pygame
class ImageHandler:
def __init__(self, maps_dir: str, base_size: int, ttl: int):
def __init__(self, maps_dir: str, base_size: int):
self.maps_dir: str = maps_dir
self.base_size: int = base_size
self.ttl: int = ttl
self.cache: dict = {}
self.history: dict[tuple[float, tuple[int, int]], float] = {}
self.size: int = 0
self.count: int = 0
self.total: int = 0
self.loading: bool = False
@ -33,7 +29,6 @@ class ImageHandler:
name, x, y = path.split(".")[0].split("_")
cache[(int(x), int(y))] = pygame.image.load(fullpath).convert_alpha()
self.count += 1
self.size += 1
self.cache = {
1: cache
@ -70,19 +65,6 @@ class ImageHandler:
img = pygame.transform.scale_by(img, zoom)
cache[pos] = img
self.size += 1
self.cache[zoom] = cache
self.history[(zoom, pos)] = time.time()
return cache[pos]
def clean(self) -> None:
t = time.time()
new_history = {}
for (zoom, pos), t0 in self.history.items():
if zoom != 1 and t0 + self.ttl < t:
del self.cache[zoom][pos]
self.size -= 1
else:
new_history[(zoom, pos)] = t0
self.history = new_history

View File

View File

@ -1,285 +0,0 @@
import json
import os
import re
import tempfile
from datetime import datetime
from ftplib import FTP
import numpy as np
import pygame
from scipy.signal import convolve2d
from src.utils.minecraft.chunk import Chunk, MalformedChunk, OldChunk
from src.utils.minecraft.region import Region
from src.utils.paths import CONFIG_DIR, CACHE_DIR, get_project_path
CHUNK_SIZE = 16
CHUNKS_IN_REGION = 32
REGION_SIZE = CHUNK_SIZE * CHUNKS_IN_REGION
GRADIENT_MAT = np.array([
[0, 0, 0],
[0, 1, 0.5],
[0, -0.5, -1]
])
GRADIENT_RANGE = 2
GAUSSIAN_MAT = np.array([
[1, 4, 7, 4, 1],
[4, 16, 26, 16, 4],
[7, 26, 41, 26, 7],
[4, 16, 26, 16, 4],
[1, 4, 7, 4, 1],
])
GAUSSIAN_FACT = GAUSSIAN_MAT.sum()
class AutoMapper:
CONFIG_PATH = os.path.join(CONFIG_DIR, "mapper.json")
CACHE_PATH = os.path.join(CACHE_DIR, "regions.txt")
COLORS_PATH = os.path.join(CACHE_DIR, "colors.json")
MAPS_DIR = os.path.join(CACHE_DIR, "maps")
MAP_SIZE = 1024
def __init__(self):
self.config: FTPConfig = FTPConfig(self.CONFIG_PATH)
self.ftp: FTP = FTP(self.config.HOST)
self.regions: list[tuple[int, int]] = []
self.temp_dir: tempfile.TemporaryDirectory[str] = tempfile.TemporaryDirectory()
self.colors: dict[str, tuple[float, float, float]] = {}
self.cache: dict[tuple[int, int], int] = {}
self.colormaps: dict[str, dict[str, tuple[float, float, float]]] = {}
self.available_regions: list[tuple[int, int]] = []
self.load_colors()
self.load_cache()
self.load_colormaps()
self.no_color: set[str] = set()
def __enter__(self):
self.ftp.login(self.config.USERNAME, self.config.PASSWORD)
self.ftp.cwd(self.config.DIR)
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.ftp.close()
def load_colors(self) -> None:
with open(self.COLORS_PATH, "r") as f:
self.colors = json.load(f)
def load_cache(self) -> None:
if os.path.exists(self.CACHE_PATH):
with open(self.CACHE_PATH, "r") as f:
lines = filter(lambda l: len(l.strip()) != 0, f.read().splitlines())
self.cache = {}
for line in lines:
rx, rz, ts = map(int, line.split(" "))
self.cache[(rx, rz)] = ts
def load_colormaps(self) -> None:
biome_ids = {}
with open(get_project_path("res", "biome_ids.txt")) as f:
lines = f.read().splitlines()
for line in lines:
if len(line.strip()) == 0:
continue
biome_name, biome_id = line.split(" ")
biome_ids[f"minecraft:{biome_name}"] = int(biome_id)
self.colormaps = {}
maps_dir = get_project_path("res", "colormaps")
for filename in os.listdir(maps_dir):
if not filename.endswith(".png"):
continue
img = pygame.image.load(os.path.join(maps_dir, filename))
block = filename.split(".")[0]
colormap = {}
for biome_name, biome_id in biome_ids.items():
colormap[biome_name] = img.get_at((biome_id, 0))[:3]
self.colormaps[f"minecraft:{block}"] = colormap
self.colormaps["minecraft:grass"] = self.colormaps["minecraft:grass_block"]
self.colormaps["minecraft:tall_grass"] = self.colormaps["minecraft:grass_block"]
self.colormaps["minecraft:vine"] = self.colormaps["minecraft:oak_leaves"]
self.colormaps["minecraft:large_fern"] = self.colormaps["minecraft:grass_block"]
self.colormaps["minecraft:fern"] = self.colormaps["minecraft:grass_block"]
self.colormaps["minecraft:melon_stem"] = self.colormaps["minecraft:foliage"]
self.colormaps["minecraft:attached_melon_stem"] = self.colormaps["minecraft:foliage"]
self.colormaps["minecraft:mangrove_leaves"] = self.colormaps["minecraft:foliage"]
self.colormaps["minecraft:bubble_column"] = self.colormaps["minecraft:water"]
def save_cache(self) -> None:
with open(self.CACHE_PATH, "w") as f:
for (rx, rz), ts in self.cache.items():
f.write(f"{rx} {rz} {ts}\n")
def get_available_regions(self) -> dict[tuple[int, int], int]:
files = self.ftp.mlsd(facts=["modify"])
regions = {}
self.available_regions = []
for filename, facts in files:
m = re.match(r"r\.(-?\d+)\.(-?\d+)\.mca", filename)
if m:
rx = int(m.group(1))
rz = int(m.group(2))
t = datetime.strptime(facts["modify"], "%Y%m%d%H%M%S")
regions[(rx, rz)] = int(t.timestamp())
self.available_regions.append((rx, rz))
return regions
def fetch_region(self, rx: int, rz: int) -> str:
name = f"r.{rx}.{rz}.mca"
outpath = os.path.join(self.temp_dir.name, name)
with open(outpath, "wb") as f:
self.ftp.retrbinary(f"RETR {name}", f.write, 1024)
return outpath
def map_region(self, rx: int, rz: int) -> tuple[pygame.Surface, np.array]:
print(f" [Fetching region ({rx},{rz})]")
path = self.fetch_region(rx, rz)
region = Region(path)
print(" [Rendering]")
surf = pygame.Surface([REGION_SIZE, REGION_SIZE], pygame.SRCALPHA)
surf.fill((0, 0, 0, 0))
heightmap = np.zeros((REGION_SIZE, REGION_SIZE), dtype="uint16")
for cz in range(CHUNKS_IN_REGION):
for cx in range(CHUNKS_IN_REGION):
ox, oy = cx * CHUNK_SIZE, cz * CHUNK_SIZE
chunk = region.get_chunk(rx * CHUNKS_IN_REGION + cx, rz * CHUNKS_IN_REGION + cz)
if isinstance(chunk, Chunk):
hm = self.render_chunk(chunk, surf, ox, oy)
heightmap[oy:oy+CHUNK_SIZE, ox:ox+CHUNK_SIZE] = hm
elif isinstance(chunk, MalformedChunk):
pygame.draw.rect(surf, (92, 47, 32, 200), [ox, oy, CHUNK_SIZE, CHUNK_SIZE])
elif isinstance(chunk, OldChunk):
pygame.draw.rect(surf, (32, 61, 92, 200), [ox, oy, CHUNK_SIZE, CHUNK_SIZE])
print()
os.remove(path)
return surf, heightmap
def map_region_group(self, x: int, z: int) -> pygame.Surface:
surf = pygame.Surface([self.MAP_SIZE, self.MAP_SIZE], pygame.SRCALPHA)
surf.fill((0, 0, 0, 0))
n_regions = self.MAP_SIZE // REGION_SIZE
heightmap = np.zeros((self.MAP_SIZE, self.MAP_SIZE), dtype="uint16")
for dz in range(n_regions):
for dx in range(n_regions):
rx, rz = x * n_regions + dx, z * n_regions + dz
if (rx, rz) in self.available_regions:
region, hm = self.map_region(rx, rz)
ox, oy = dx * REGION_SIZE, dz * REGION_SIZE
surf.blit(region, [ox, oy])
heightmap[oy:oy+REGION_SIZE, ox:ox+REGION_SIZE] = hm
self.cache[(rx, rz)] = int(datetime.utcnow().timestamp())
gradient = convolve2d(heightmap, GRADIENT_MAT, boundary="symm")[1:self.MAP_SIZE+1, 1:self.MAP_SIZE+1]
gradient = gradient.clip(-GRADIENT_RANGE, GRADIENT_RANGE)
gradient = 1 + gradient / 2 / GRADIENT_RANGE
gradient = np.array([gradient, gradient, gradient, np.ones(gradient.shape)])
gradient = np.swapaxes(gradient, 0, 2)
surf_array = pygame.surfarray.array3d(surf)
alpha_array = pygame.surfarray.array_alpha(surf).reshape((*surf_array.shape[0:2], 1))
surf_array = np.concatenate((surf_array, alpha_array), 2)
res = surf_array * gradient
res = res.clip(0, 255)
buf = res.transpose((1, 0, 2)).astype("uint8").tobytes(order="C")
surf = pygame.image.frombuffer(buf, surf.get_size(), "RGBA")
return surf
def render_chunk(self, chunk: Chunk, surf: pygame.Surface, ox: int, oy: int) -> np.array:
blocks, heightmap, biomes, is_empty = chunk.get_top_blocks()
if is_empty:
pygame.draw.rect(surf, (0, 0, 0, 0), [ox, oy, CHUNK_SIZE, CHUNK_SIZE])
else:
for z in range(CHUNK_SIZE):
for x in range(CHUNK_SIZE):
color = self.get_color(blocks[z][x], biomes[z][x])
surf.set_at((ox + x, oy + z), color)
return heightmap
def get_color(self, block: str, biome: str) -> tuple[float, float, float, float]:
if block in self.colormaps:
r, g, b = self.colormaps[block][biome]
r2, g2, b2 = self.colors.get(block, (0, 0, 0, 0))
return min(255.0, r*r2/255), min(255.0, g*g2/255), min(255.0, b*b2/255), 255
if block not in self.colors:
if block not in self.no_color:
print(f" no color for {block}")
self.no_color.add(block)
return self.colors.get(block, (0, 0, 0, 0))
def map_world(self) -> None:
if not os.path.exists(self.MAPS_DIR):
os.mkdir(self.MAPS_DIR)
regions = self.get_available_regions()
groups_to_map = set()
n_regions = self.MAP_SIZE // REGION_SIZE
for pos, modified_at in regions.items():
if pos in self.cache and modified_at <= self.cache[pos]:
continue
gx = pos[0] // n_regions
gz = pos[1] // n_regions
groups_to_map.add((gx, gz))
n_groups = len(groups_to_map)
print(f"Groups to map: {n_groups}")
proceed = input("Proceed ? y/[N] ")
if proceed.strip().lower() == "y":
groups_to_map = sorted(groups_to_map, key=lambda g: g[0]*g[0] + g[1]*g[1])
for i, (gx, gz) in enumerate(groups_to_map):
print(f"[Mapping group ({gx}, {gz}) ({i+1}/{n_groups})]")
try:
surf = self.map_region_group(gx, gz)
pygame.image.save(surf, os.path.join(self.MAPS_DIR, f"map_{gx}_{gz}.png"))
self.save_cache()
except Exception as e:
raise e
class FTPConfig:
HOST = ""
USERNAME = ""
PASSWORD = ""
DIR = ""
def __init__(self, path: str):
self._path: str = path
self.load()
def load(self) -> None:
if os.path.exists(self._path):
with open(self._path, "r") as f:
config = json.load(f)
self.HOST = config["host"]
self.USERNAME = config["username"]
self.PASSWORD = config["password"]
self.DIR = config["dir"]
def save(self) -> None:
with open(self._path, "w") as f:
json.dump({
"host": self.HOST,
"username": self.USERNAME,
"password": self.PASSWORD,
"dir": self.DIR
}, f, indent=4)
if __name__ == '__main__':
pygame.init()
# x = -1
# z = 0
with AutoMapper() as mapper:
# mapper.get_available_regions()
# surf = mapper.map_region_group(x, z)
# pygame.image.save(surf, f"/tmp/map_{x}_{z}.png")
mapper.map_world()

View File

@ -1,244 +0,0 @@
import json
import os
import platform
import re
import tempfile
import zipfile
import pygame
from src.utils.paths import get_project_path, CACHE_DIR
# Textures with these parts will be excluded
EXCLUDE = {
"bottom", "side", "front", "destroy", "on", "tip", "lit", "inner"
}
# Textures with these names will be copied to their variants (variants listed in `VARIANTS`)
# Tuples indicate a change of name for variants
TO_VARIANT = [
("nether_bricks", "nether_brick"),
("nether_bricks", "nether_brick"),
("oak_planks", "oak"),
("spruce_planks", "spruce"),
("birch_planks", "birch"),
("jungle_planks", "jungle"),
("acacia_planks", "acacia"),
("dark_oak_planks", "dark_oak"),
("mangrove_planks", "mangrove"),
("cherry_planks", "cherry"),
("bamboo_mosaic", "bamboo"),
("crimson_planks", "crimson"),
("warped_planks", "warped"),
"stone",
"cobblestone",
"mossy_cobblestone",
"smooth_stone",
("stone_bricks", "stone_brick"),
("mossy_stone_bricks", "mossy_stone_brick"),
"granite",
"polished_granite",
"diorite",
"polished_diorite",
"andesite",
"polished_andesite",
"cobbled_deepslate",
"polished_deepslate",
("deepslate_bricks", "deepslate_brick"),
("deepslate_tiles", "deepslate_tile"),
("bricks", "brick"),
("mud_bricks", "mud_brick"),
"sandstone",
"smooth_sandstone",
"cut_sandstone",
"red_sandstone",
"smooth_red_sandstone",
"cut_red_sandstone",
"prismarine",
("prismarine_bricks", "prismarine_brick"),
"dark_prismarine",
("red_nether_bricks", "red_nether_brick"),
"blackstone",
"polished_blackstone",
("polished_blackstone_bricks", "polished_blackstone_brick"),
("end_stone_bricks", "end_stone_brick"),
("purpur_block", "purpur"),
("quartz_block", "quartz"),
"smooth_quartz",
"cut_copper",
"exposed_cut_copper",
"weathered_cut_copper",
"oxidized_cut_copper",
"waxed_cut_copper",
"waxed_exposed_cut_copper",
"waxed_weathered_cut_copper",
"waxed_oxidized_cut_copper",
]
# Variants of the textures in `TO_VARIANT`
VARIANTS = [
"slab", "stairs",
"wall", "fence", "fence_gate",
"pressure_plate", "button",
"sign", "wall_sign", "hanging_sign", "wall_hanging_sign"
]
# Colors to copy
TO_COPY = [
("minecraft:furnace", "minecraft:dropper"),
("minecraft:furnace", "minecraft:dispenser"),
("minecraft:furnace", "minecraft:piston"),
("minecraft:furnace", "minecraft:sticky_piston"),
("minecraft:oak_planks", "minecraft:piston_head"),
("minecraft:oak_planks", "minecraft:sticky_piston_head"),
("minecraft:torch", "minecraft:wall_torch"),
("minecraft:soul_torch", "minecraft:soul_wall_torch"),
("minecraft:redstone_torch", "minecraft:redstone_wall_torch"),
("minecraft:snow", "minecraft:snow_block"),
("minecraft:water", "minecraft:bubble_column"),
("minecraft:sandstone", "minecraft:smooth_sandstone"),
("minecraft:red_sandstone", "minecraft:smooth_red_sandstone"),
("minecraft:quartz_block", "minecraft:smooth_quartz"),
("minecraft:dripstone_block", "minecraft:pointed_dripstone"),
("minecraft:oak_log", "minecraft:oak_wood"),
("minecraft:spruce_log", "minecraft:spruce_wood"),
("minecraft:birch_log", "minecraft:birch_wood"),
("minecraft:acacia_log", "minecraft:acacia_wood"),
("minecraft:jungle_log", "minecraft:jungle_wood"),
("minecraft:cherry_log", "minecraft:cherry_wood"),
("minecraft:mangrove_log", "minecraft:mangrove_wood"),
("minecraft:dark_oak_log", "minecraft:dark_oak_wood"),
("minecraft:stripped_oak_log", "minecraft:stripped_oak_wood"),
("minecraft:stripped_spruce_log", "minecraft:stripped_spruce_wood"),
("minecraft:stripped_birch_log", "minecraft:stripped_birch_wood"),
("minecraft:stripped_acacia_log", "minecraft:stripped_acacia_wood"),
("minecraft:stripped_jungle_log", "minecraft:stripped_jungle_wood"),
("minecraft:stripped_cherry_log", "minecraft:stripped_cherry_wood"),
("minecraft:stripped_mangrove_log", "minecraft:stripped_mangrove_wood"),
("minecraft:stripped_dark_oak_log", "minecraft:stripped_dark_oak_wood"),
("minecraft:magma", "minecraft:magma_block"),
("minecraft:cut_copper", "minecraft:waxed_cut_copper"),
("minecraft:exposed_cut_copper", "minecraft:waxed_exposed_cut_copper"),
("minecraft:weathered_cut_copper", "minecraft:waxed_weathered_cut_copper"),
("minecraft:oxidized_cut_copper", "minecraft:waxed_oxidized_cut_copper"),
("minecraft:iron_block", "minecraft:heavy_weighted_pressure_plate"),
("minecraft:gold_block", "minecraft:light_weighted_pressure_plate"),
("minecraft:bricks", "minecraft:flower_pot"),
("minecraft:oak_log", "minecraft:campfire"),
("minecraft:oak_log", "minecraft:soul_campfire"),
("minecraft:moss_block", "minecraft:moss_carpet"),
("minecraft:stone", "minecraft:infested_stone"),
("minecraft:cobblestone", "minecraft:infested_cobblestone"),
("minecraft:stone_bricks", "minecraft:infested_stone_bricks"),
("minecraft:mossy_stone_bricks", "minecraft:infested_mossy_stone_bricks"),
("minecraft:chiseled_stone_bricks", "minecraft:infested_chiseled_stone_bricks"),
("minecraft:deepslate", "minecraft:infested_deepslate"),
("minecraft:infested_stone_bricks", "minecraft:cracked_infested_stone_bricks"),
("minecraft:cauldron", "minecraft:water_cauldron"),
("minecraft:cauldron", "minecraft:lava_cauldron"),
("minecraft:cauldron", "minecraft:powder_snow_cauldron"),
]
# Wool colors
WOOLS = [
"red", "blue", "cyan", "gray",
"lime", "pink", "black", "brown",
"green", "white", "orange", "purple",
"yellow", "magenta", "light_blue", "light_gray"
]
# Wool variants
WOOL_VARIANTS = [
"carpet", "bed", "banner", "wall_banner"
]
# These will be removed from the textures' names
TO_STRIP = ["_top", "_stalk", "_end", "_round", "_still"]
# Minecraft version
MC_VERSION = "1.20.1"
# Minecraft root directory (platform dependent)
MC_ROOT = os.path.expanduser({
"Linux": r"~/.minecraft",
"Darwin": r"~/Library/Application Support/minecraft",
"Windows": r"%APPDATA%\.minecraft"
}[platform.system()])
def extract_textures(jar_path: str, outdir: str) -> None:
with zipfile.ZipFile(jar_path) as f:
for info in f.infolist():
path = info.filename
if not re.match(r"^assets/minecraft/textures/block/[^/]+\.png$", path):
continue
info.filename = os.path.basename(path)
f.extract(info, outdir)
def main() -> None:
print(f"[1/5] Extracting Minecraft {MC_VERSION} textures")
jar_path = os.path.join(MC_ROOT, "versions", MC_VERSION, f"{MC_VERSION}.jar")
if not os.path.exists(jar_path):
print(f"Couldn't find Minecraft {MC_VERSION} JAR file")
print(f"Not at {jar_path}")
return None
workdir = tempfile.TemporaryDirectory()
extract_textures(jar_path, workdir.name)
pygame.init()
pygame.display.set_mode((1, 1), pygame.NOFRAME | pygame.HIDDEN)
colors = {}
paths = os.listdir(workdir.name)
total = len(paths)
skipped = 0
print("[2/5] Averaging textures")
for i, filename in enumerate(paths):
print(f"\r{i+1}/{total} ({i/total*100:.2f}%) {filename}", end="")
block, ext = filename.rsplit(".", 1)
parts = set(block.split("_"))
if not parts.isdisjoint(EXCLUDE):
skipped += 1
continue
for s in TO_STRIP:
block = block.replace(s, "")
img = pygame.image.load(os.path.join(workdir.name, filename)).convert_alpha()
color = pygame.transform.average_color(img, consider_alpha=True)
color = color[:3]
colors[f"minecraft:{block}"] = color
print(f"\r{total}/{total} (100%) Finished")
print(f"Skipped {skipped} files")
print("[3/5] Applying overrides")
with open(get_project_path("res", "color_overrides.json"), "r") as f:
overrides = json.load(f)
colors.update(overrides)
print("[4/5] Generating variants")
for to_variant in TO_VARIANT:
src = to_variant[0] if isinstance(to_variant, tuple) else to_variant
dst = to_variant[1] if isinstance(to_variant, tuple) else to_variant
for variant in VARIANTS:
TO_COPY.append((f"minecraft:{src}", f"minecraft:{dst}_{variant}"))
for color in WOOLS:
for variant in WOOL_VARIANTS:
TO_COPY.append((f"minecraft:{color}_wool", f"minecraft:{color}_{variant}"))
for src, dst in TO_COPY:
colors[dst] = colors[src]
print("[5/5] Exporting colors")
outpath = os.path.join(CACHE_DIR, "colors.json")
with open(outpath, "w") as f:
json.dump(colors, f, indent=4)
if __name__ == '__main__':
main()

View File

@ -1,19 +0,0 @@
from typing import Optional
from nbt.nbt import TAG
class Block:
def __init__(self,
name: str,
nbt: Optional[TAG] = None,
tile_entity: Optional[TAG] = None,
x: int = 0,
y: int = 0,
z: int = 0):
self.name: str = name
self.nbt: Optional[TAG] = nbt
self.tile_entity: Optional[TAG] = tile_entity
self.x: int = x
self.y: int = y
self.z: int = z

View File

@ -1,138 +0,0 @@
from typing import Optional
import numpy as np
from nbt.nbt import NBTFile, TAG_Compound, TAG_String
class PositionOutOfBounds(ValueError):
pass
class BlockNotFound(Exception):
pass
class ChunkBase:
def __init__(self, x: int, z: int):
self.x: int = x
self.z: int = z
class Chunk(ChunkBase):
def __init__(self, x: int, z: int, nbt: NBTFile):
super().__init__(x, z)
self.ox: int = x * 16
self.oy: int = nbt.get("yPos").value * 16
self.oz: int = z * 16
self.nbt: NBTFile = nbt
self.palettes: list[list[TAG_Compound]] = []
self.blockstates: list[list[int]] = []
self.biome_palettes: list[list[TAG_String]] = []
self.biomes: list[list[int]] = []
self.get_sections()
def get_sections(self) -> None:
self.palettes = []
self.blockstates = []
self.biome_palettes = []
self.biomes = []
sections = self.nbt.get("sections")
for s in range(1, len(sections)-1):
section = sections[s]
bs_tag = section.get("block_states")
self.palettes.append(list(bs_tag.get("palette")))
bs = bs_tag.get("data")
self.blockstates.append([] if bs is None else list(bs))
biomes_tag = section.get("biomes")
self.biome_palettes.append(list(biomes_tag.get("palette")))
biomes = biomes_tag.get("data")
self.biomes.append([] if biomes is None else list(biomes))
def get_block(self, x: int, y: int, z: int) -> Optional[tuple[str, str]]:
if 0 <= x < 16 and 0 <= z < 16:
section_i = y // 16 + 4
oy = y // 16 * 16
palette = self.palettes[section_i - 1]
blockstates = self.blockstates[section_i - 1]
biome_palette = self.biome_palettes[section_i - 1]
biomes = self.biomes[section_i - 1]
if blockstates is None or len(blockstates) == 0:
return palette[0].get("Name").value, biome_palette[0].value
rel_y = y - oy
block_bits = max((len(palette) - 1).bit_length(), 4)
state = self.get_from_long_array(blockstates, block_bits, x, rel_y, z)
block = palette[state]
if len(biome_palette) == 1:
biome_state = 0
else:
biome_bits = (len(biome_palette) - 1).bit_length()
biome_state = self.get_from_long_array(biomes, biome_bits, x // 4, rel_y // 4, z // 4, 4)
biome = biome_palette[biome_state]
return block.get("Name").value, biome.value
else:
raise PositionOutOfBounds(f"Coordinates x and z should be in range [0:16[")
def get_from_long_array(self, long_array, bits, x, y, z, size=16) -> int:
ids_per_long = 64 // bits
block_i = y * size * size + z * size + x
long_i = block_i // ids_per_long
long_val = long_array[long_i]
if long_val < 0:
long_val += 2**64
bit_i = (block_i % ids_per_long) * bits
value = (long_val >> bit_i) & ((1 << bits) - 1)
return value
def get_top_blocks(self) -> tuple[list[list[str]], np.array, np.array, bool]:
blocks = [[] for _ in range(16)]
heightmap_shadow = np.zeros((16, 16), dtype="uint16")
heightmap = np.zeros((16, 16), dtype="uint16")
biomes = [["minecraft:plains"]*16 for _ in range(16)]
heightmap_shadow_longs = self.nbt.get("Heightmaps").get("MOTION_BLOCKING")
heightmap_longs = self.nbt.get("Heightmaps").get("WORLD_SURFACE")
if heightmap_longs is None:
return [["minecraft:air"]*16 for _ in range(16)], heightmap_shadow, biomes, True
heightmap_shadow_longs = heightmap_shadow_longs.value
heightmap_longs = heightmap_longs.value
i = 0
for z in range(16):
for x in range(16):
# i = z * 16 + x
long_i = i // 7
long1_val = heightmap_shadow_longs[long_i]
long2_val = heightmap_longs[long_i]
bit_i = (i % 7) * 9
height_shadow = (long1_val >> bit_i) & 0b1_1111_1111
height = (long2_val >> bit_i) & 0b1_1111_1111
heightmap_shadow[z, x] = height_shadow
heightmap[z, x] = height
y = self.oy + height - 1
if height == 0:
block = "minecraft:air"
biome = "minecraft:plains"
else:
block, biome = self.get_block(x, y, z)
blocks[z].append(block)
biomes[z][x] = biome
i += 1
return blocks, heightmap_shadow, biomes, False
class OldChunk(ChunkBase):
def __init__(self, x: int, z: int, data_version: int):
super().__init__(x, z)
self.data_version: int = data_version
class MalformedChunk(ChunkBase):
pass

View File

@ -1,77 +0,0 @@
import zlib
from typing import Optional
import nbt
from src.utils.minecraft.chunk import Chunk, ChunkBase, MalformedChunk, OldChunk
def to_int(bytes_):
return int.from_bytes(bytes_, byteorder="big")
class Region:
DATA_VERSION = 3465
def __init__(self, filepath):
self.file = open(filepath, "rb")
self.locations: list[tuple[int, int]] = self.get_locations()
self.timestamps: list[int] = self.get_timestamps()
self.chunks = {}
def get_locations(self) -> list[tuple[int, int]]:
self.file.seek(0)
locations = []
for c in range(1024):
offset = to_int(self.file.read(3))
length = to_int(self.file.read(1))
locations.append((offset, length))
return locations
def get_chunk(self, x, z) -> ChunkBase:
if (x, z) in self.chunks.keys():
return self.chunks[(x, z)]
loc = self.locations[(x % 32) + (z % 32) * 32]
self.file.seek(loc[0] * 4096)
length = to_int(self.file.read(4))
compression = to_int(self.file.read(1))
data = self.file.read(length-1)
if compression == 2:
try:
data = zlib.decompress(data)
except zlib.error:
return MalformedChunk(x, z)
else:
# print(f"{compression} is not a valid compression type")
return MalformedChunk(x, z)
chunk = nbt.nbt.NBTFile(buffer=nbt.chunk.BytesIO(data))
data_version = chunk.get("DataVersion").value
if data_version != self.DATA_VERSION:
print(f"\r Invalid data version {data_version} for chunk ({x},{z})", end="")
return OldChunk(x, z, data_version)
chunk = Chunk(x, z, chunk)
self.chunks[(x, z)] = chunk
return chunk
def get_timestamps(self) -> list[int]:
return []
def get_block(self, x: int, y: int, z: int) -> Optional[str]:
chunk_x, chunk_z = x//16, z//16
x_rel, z_rel = x % 16, z % 16
chunk = self.get_chunk(chunk_x, chunk_z)
if not isinstance(chunk, Chunk):
return None
return chunk.get_block(x_rel, y, z_rel)

View File

@ -1,14 +0,0 @@
import os.path
import platformdirs
APP_NAME = "lycacraft-paths"
APP_AUTHOR = "lycacraft"
CACHE_DIR = platformdirs.user_cache_dir(appname=APP_NAME, appauthor=APP_AUTHOR, ensure_exists=True)
CONFIG_DIR = platformdirs.user_config_dir(appname=APP_NAME, appauthor=APP_AUTHOR, ensure_exists=True)
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir))
def get_project_path(*elmts: str) -> str:
return os.path.join(ROOT, *elmts)

View File

Before

Width:  |  Height:  |  Size: 20 MiB

After

Width:  |  Height:  |  Size: 20 MiB

88
utils/dijkstra.py Normal file
View File

@ -0,0 +1,88 @@
from math import inf
from typing import Iterator, Optional
class Node:
def __init__(self, x: int, y: int):
self.x: int = x
self.y: int = y
class Edge:
def __init__(self, start: int, end: int, length: float):
self.length: float = length
self.start: int = start
self.end: int = end
class Graph:
def __init__(self):
self.edges: list[Edge] = []
self.nodes: list[Node] = []
def add_node(self, x: int, y: int) -> None:
self.nodes.append(Node(x, y))
def add_edge(self, start_index: int, end_index: int, length: float) -> None:
self.edges.append(Edge(start_index, end_index, length))
def edges_adjacent_to(self, node_i: int) -> Iterator[Edge]:
return filter(lambda e: e.start == node_i or e.end == node_i, self.edges)
def dijkstra(self, source_index: int, target_index: int) -> Optional[list[int]]:
n = len(self.nodes)
if source_index < 0 or source_index >= n:
return None
if target_index < 0 or target_index >= n:
return None
unvisited = list(range(n))
distances_from_start = [inf] * n
distances_from_start[source_index] = 0
node_sequences = [[] for _ in range(n)]
node_sequences[source_index] = [source_index]
while True:
current_index = min(unvisited, key=lambda i: distances_from_start[i])
if current_index == target_index:
break
unvisited.remove(current_index)
for edge in self.edges_adjacent_to(current_index):
start = current_index
end = edge.end if edge.start == current_index else edge.start
if end in unvisited and distances_from_start[end] > distances_from_start[start] + edge.length:
distances_from_start[end] = distances_from_start[start] + edge.length
node_sequences[end] = node_sequences[start].copy()
node_sequences[end].append(end)
return node_sequences[target_index]
def main() -> None:
graph = Graph()
graph.add_node(1, 2)
graph.add_node(4, 7)
graph.add_node(3, 1)
graph.add_node(-2, 0)
graph.add_node(0, 0)
graph.add_edge(0, 1, 1)
graph.add_edge(1, 2, 2)
graph.add_edge(2, 3, 3)
graph.add_edge(3, 0, 1)
graph.add_edge(1, 3, 3)
print(graph.dijkstra(0, 3))
if __name__ == "__main__":
main()

View File

@ -8,13 +8,12 @@ Image.MAX_IMAGE_PIXELS = 200000000
MAP_SIZE = 1024
DEFAULT_PATH = os.path.join(platformdirs.user_cache_dir(appname="lycacraft-paths", appauthor="Lycacraft"), "maps")
def clamp(mn, value, mx):
return max(mn, min(mx, value))
def main():
# src/utils/2024-06-27_21.01.45_Lycacraft_minecraft~overworld_day.png
# utils/2024-06-27_21.01.45_Lycacraft_minecraft~overworld_day.png
# 6144,10240
input_path = input("Input image: ")
output_path = input(f"Output dir (default: {DEFAULT_PATH}): ")