From a5943128f27303d200690f250e667e62d1706176 Mon Sep 17 00:00:00 2001 From: robcholz <84130577+robcholz@users.noreply.github.com> Date: Tue, 23 Dec 2025 01:44:51 -0500 Subject: [PATCH 1/5] feat: added logic --- feature/mc_mod/.gitignore | 119 +++++++++ feature/mc_mod/build.gradle | 92 +++++++ feature/mc_mod/gradle.properties | 16 ++ .../mc_mod/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 45633 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + feature/mc_mod/gradlew | 248 +++++++++++++++++ feature/mc_mod/gradlew.bat | 93 +++++++ feature/mc_mod/settings.gradle | 9 + .../com/robcholz/lumen/LumenSyncServer.java | 121 +++++++++ .../com/robcholz/lumen/LumenSyncState.java | 251 ++++++++++++++++++ .../robcholz/lumen/client/LumenClient.java | 21 ++ .../src/main/resources/assets/lumen/icon.png | Bin 0 -> 1689 bytes .../mc_mod/src/main/resources/fabric.mod.json | 28 ++ 13 files changed, 1005 insertions(+) create mode 100644 feature/mc_mod/.gitignore create mode 100644 feature/mc_mod/build.gradle create mode 100644 feature/mc_mod/gradle.properties create mode 100644 feature/mc_mod/gradle/wrapper/gradle-wrapper.jar create mode 100644 feature/mc_mod/gradle/wrapper/gradle-wrapper.properties create mode 100755 feature/mc_mod/gradlew create mode 100644 feature/mc_mod/gradlew.bat create mode 100644 feature/mc_mod/settings.gradle create mode 100644 feature/mc_mod/src/client/java/com/robcholz/lumen/LumenSyncServer.java create mode 100644 feature/mc_mod/src/client/java/com/robcholz/lumen/LumenSyncState.java create mode 100644 feature/mc_mod/src/client/java/com/robcholz/lumen/client/LumenClient.java create mode 100644 feature/mc_mod/src/main/resources/assets/lumen/icon.png create mode 100644 feature/mc_mod/src/main/resources/fabric.mod.json diff --git a/feature/mc_mod/.gitignore b/feature/mc_mod/.gitignore new file mode 100644 index 0000000..d5f737e --- /dev/null +++ b/feature/mc_mod/.gitignore @@ -0,0 +1,119 @@ +# User-specific stuff +.idea/ + +*.iml +*.ipr +*.iws + +# IntelliJ +out/ +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +.gradle +build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Cache of project +.gradletasknamecache + +**/build/ + +# Common working directory +run/ +runs/ + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar diff --git a/feature/mc_mod/build.gradle b/feature/mc_mod/build.gradle new file mode 100644 index 0000000..f66176b --- /dev/null +++ b/feature/mc_mod/build.gradle @@ -0,0 +1,92 @@ +plugins { + id 'fabric-loom' version '1.13.6' + id 'maven-publish' +} + +version = project.mod_version +group = project.maven_group + +base { + archivesName = "${project.archives_base_name}+${project.fabric_version}" +} + +repositories { + maven { + name = "Terraformers" + url = "https://maven.terraformersmc.com/" + } +} + +loom { + splitEnvironmentSourceSets() + + mods { + "lumen" { + sourceSet sourceSets.main + sourceSet sourceSets.client + } + } + +} + +dependencies { + // To change the versions see the gradle.properties file + minecraft "com.mojang:minecraft:${project.minecraft_version}" + mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2" + modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" + + modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}" + modImplementation "com.terraformersmc:modmenu:${project.modmenu_version}" +} + +processResources { + inputs.property "version", project.version + inputs.property "minecraft_version", project.minecraft_version + inputs.property "loader_version", project.loader_version + + filesMatching("fabric.mod.json") { + expand "version": project.version, + "minecraft_version": project.minecraft_version, + "loader_version": project.loader_version + } +} + +tasks.withType(JavaCompile).configureEach { + it.options.release = 21 +} + +java { + // Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task + // if it is present. + // If you remove this line, sources will not be generated. + withSourcesJar() + + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 +} + +jar { + inputs.property "archivesName", project.base.archivesName + + from("LICENSE") { + rename { "${it}_${inputs.properties.archivesName}" } + } +} + +// configure the maven publication +publishing { + publications { + create("mavenJava", MavenPublication) { + artifactId = project.archives_base_name + from components.java + } + } + + // See https://docs.gradle.org/current/userguide/publishing_maven.html for information on how to set up publishing. + repositories { + // Add repositories to publish to here. + // Notice: This block does NOT have the same function as the block in the top level. + // The repositories here will be used for publishing your artifact, not for + // retrieving dependencies. + } +} diff --git a/feature/mc_mod/gradle.properties b/feature/mc_mod/gradle.properties new file mode 100644 index 0000000..fa81590 --- /dev/null +++ b/feature/mc_mod/gradle.properties @@ -0,0 +1,16 @@ +# Done to increase the memory available to gradle. +org.gradle.jvmargs=-Xmx1G +# Fabric Properties +# check these on https://modmuss50.me/fabric.html +minecraft_version=1.21.3 +yarn_mappings=1.21.3+build.2 +loader_version=0.17.2 +loom_version=1.12 +# Mod Properties +mod_version=1.0.0 +maven_group=com.robcholz +archives_base_name=lumen +# Dependencies +# check this on https://modmuss50.me/fabric.html +fabric_version=0.112.1+1.21.3 +modmenu_version=12.0.0 diff --git a/feature/mc_mod/gradle/wrapper/gradle-wrapper.jar b/feature/mc_mod/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..f8e1ee3125fe0768e9a76ee977ac089eb657005e GIT binary patch literal 45633 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�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 literal 0 HcmV?d00001 diff --git a/feature/mc_mod/gradle/wrapper/gradle-wrapper.properties b/feature/mc_mod/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..23449a2 --- /dev/null +++ b/feature/mc_mod/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/feature/mc_mod/gradlew b/feature/mc_mod/gradlew new file mode 100755 index 0000000..adff685 --- /dev/null +++ b/feature/mc_mod/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/feature/mc_mod/gradlew.bat b/feature/mc_mod/gradlew.bat new file mode 100644 index 0000000..c4bdd3a --- /dev/null +++ b/feature/mc_mod/gradlew.bat @@ -0,0 +1,93 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/feature/mc_mod/settings.gradle b/feature/mc_mod/settings.gradle new file mode 100644 index 0000000..f91a4fe --- /dev/null +++ b/feature/mc_mod/settings.gradle @@ -0,0 +1,9 @@ +pluginManagement { + repositories { + maven { + name = 'Fabric' + url = 'https://maven.fabricmc.net/' + } + gradlePluginPortal() + } +} diff --git a/feature/mc_mod/src/client/java/com/robcholz/lumen/LumenSyncServer.java b/feature/mc_mod/src/client/java/com/robcholz/lumen/LumenSyncServer.java new file mode 100644 index 0000000..e86fd10 --- /dev/null +++ b/feature/mc_mod/src/client/java/com/robcholz/lumen/LumenSyncServer.java @@ -0,0 +1,121 @@ +package com.robcholz.lumen; + +import com.google.gson.JsonObject; +import com.sun.net.httpserver.Headers; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpServer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.NetworkInterface; +import java.nio.charset.StandardCharsets; +import java.util.Enumeration; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public final class LumenSyncServer { + private static final Logger LOGGER = LoggerFactory.getLogger(LumenSyncServer.class); + private static final int PORT = 47123; + + private static HttpServer server; + + private LumenSyncServer() { + } + + public static synchronized void start() throws IOException { + if (server != null) { + return; + } + + InetAddress bindAddress = resolveBindAddress(); + server = HttpServer.create(new InetSocketAddress(bindAddress, PORT), 0); + server.createContext("/lumen/sync", LumenSyncServer::handleSync); + server.createContext("/lumen/sync/skin", LumenSyncServer::handleSkin); + ExecutorService executor = Executors.newSingleThreadExecutor(r -> { + Thread thread = new Thread(r, "lumen-sync-http"); + thread.setDaemon(true); + return thread; + }); + server.setExecutor(executor); + server.start(); + String host = server.getAddress().getAddress().getHostAddress(); + LOGGER.info("Lumen sync HTTP server listening on http://{}:{}/lumen/sync", host, PORT); + LOGGER.info("Lumen sync HTTP server listening on http://{}:{}/lumen/sync/skin", host, PORT); + } + + private static void handleSync(HttpExchange exchange) throws IOException { + try { + if (!"GET".equalsIgnoreCase(exchange.getRequestMethod())) { + sendJson(exchange, 405, "{\"error\":\"method_not_allowed\"}"); + return; + } + + LumenSyncState.Snapshot snapshot = LumenSyncState.requestSnapshot(false); + JsonObject payload = new JsonObject(); + payload.addProperty("mode", snapshot.mode()); + payload.addProperty("health", snapshot.health()); + payload.addProperty("max_health", snapshot.maxHealth()); + sendJson(exchange, 200, payload.toString()); + } catch (Exception e) { + LOGGER.warn("Failed to handle /lumen/sync request", e); + sendJson(exchange, 500, "{\"error\":\"server_error\"}"); + } finally { + exchange.close(); + } + } + + private static void handleSkin(HttpExchange exchange) throws IOException { + try { + if (!"GET".equalsIgnoreCase(exchange.getRequestMethod())) { + sendJson(exchange, 405, "{\"error\":\"method_not_allowed\"}"); + return; + } + + LumenSyncState.Snapshot snapshot = LumenSyncState.requestSnapshot(true); + JsonObject payload = new JsonObject(); + payload.addProperty("skin_width", snapshot.skinWidth()); + payload.addProperty("skin_height", snapshot.skinHeight()); + payload.addProperty("skin", snapshot.skinBase64()); + sendJson(exchange, 200, payload.toString()); + } catch (Exception e) { + LOGGER.warn("Failed to handle /lumen/sync/skin request", e); + sendJson(exchange, 500, "{\"error\":\"server_error\"}"); + } finally { + exchange.close(); + } + } + + private static void sendJson(HttpExchange exchange, int status, String body) throws IOException { + Headers headers = exchange.getResponseHeaders(); + headers.set("Content-Type", "application/json; charset=utf-8"); + byte[] payload = body.getBytes(StandardCharsets.UTF_8); + exchange.sendResponseHeaders(status, payload.length); + try (OutputStream output = exchange.getResponseBody()) { + output.write(payload); + } + } + + private static InetAddress resolveBindAddress() throws IOException { + Enumeration interfaces = NetworkInterface.getNetworkInterfaces(); + while (interfaces.hasMoreElements()) { + NetworkInterface iface = interfaces.nextElement(); + if (!iface.isUp() || iface.isLoopback() || iface.isVirtual()) { + continue; + } + Enumeration addresses = iface.getInetAddresses(); + while (addresses.hasMoreElements()) { + InetAddress address = addresses.nextElement(); + if (address instanceof Inet4Address && !address.isLoopbackAddress()) { + return address; + } + } + } + LOGGER.warn("No non-loopback IPv4 address found; binding to 0.0.0.0"); + return InetAddress.getByName("0.0.0.0"); + } +} diff --git a/feature/mc_mod/src/client/java/com/robcholz/lumen/LumenSyncState.java b/feature/mc_mod/src/client/java/com/robcholz/lumen/LumenSyncState.java new file mode 100644 index 0000000..327e781 --- /dev/null +++ b/feature/mc_mod/src/client/java/com/robcholz/lumen/LumenSyncState.java @@ -0,0 +1,251 @@ +package com.robcholz.lumen; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.AbstractClientPlayerEntity; +import net.minecraft.client.texture.AbstractTexture; +import net.minecraft.client.texture.NativeImage; +import net.minecraft.client.texture.ResourceTexture; +import net.minecraft.client.util.SkinTextures; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.util.Identifier; +import net.minecraft.world.GameMode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Base64; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +public final class LumenSyncState { + private static final Logger LOGGER = LoggerFactory.getLogger(LumenSyncState.class); + + private LumenSyncState() { + } + + public static Snapshot requestSnapshot(boolean includeSkin) { + MinecraftClient client = MinecraftClient.getInstance(); + if (client == null) { + return Snapshot.defaultSnapshot(); + } + CompletableFuture future = new CompletableFuture<>(); + client.execute(() -> future.complete(captureSnapshot(client, includeSkin))); + try { + return future.get(2, TimeUnit.SECONDS); + } catch (Exception e) { + LOGGER.debug("Failed to capture snapshot", e); + return Snapshot.defaultSnapshot(); + } + } + + private static Snapshot captureSnapshot(MinecraftClient client, boolean includeSkin) { + if (client.player == null) { + return Snapshot.defaultSnapshot(); + } + + PlayerEntity player = client.player; + String mode = resolveMode(client); + double health = player.getHealth(); + double maxHealth = player.getMaxHealth(); + SkinData skin = includeSkin ? readSkin(client, player) : SkinData.empty(); + + return new Snapshot(mode, health, maxHealth, skin.width(), skin.height(), skin.base64()); + } + + private static String resolveMode(MinecraftClient client) { + if (client.interactionManager == null) { + return "survival"; + } + GameMode mode = client.interactionManager.getCurrentGameMode(); + if (mode == null) { + return "survival"; + } + return switch (mode) { + case CREATIVE -> "creative"; + case ADVENTURE -> "adventure"; + case SPECTATOR -> "spectator"; + default -> "survival"; + }; + } + + private static SkinData readSkin(MinecraftClient client, PlayerEntity player) { + if (!(player instanceof AbstractClientPlayerEntity clientPlayer)) { + return SkinData.empty(); + } + SkinTextures textures = clientPlayer.getSkinTextures(); + if (textures == null || textures.texture() == null) { + return SkinData.empty(); + } + AbstractTexture texture = client.getTextureManager().getTexture(textures.texture()); + if (texture instanceof ResourceTexture resourceTexture) { + Optional image = encodeResourceTexture(client, resourceTexture); + if (image.isEmpty()) { + return SkinData.empty(); + } + NativeImage frontView = image.get(); + Path tempFile = null; + try { + tempFile = Files.createTempFile("lumen-skin-front", ".png"); + frontView.writeTo(tempFile); + byte[] bytes = Files.readAllBytes(tempFile); + String base64 = Base64.getEncoder().encodeToString(bytes); + return new SkinData(frontView.getWidth(), frontView.getHeight(), base64); + } catch (IOException e) { + LOGGER.debug("Failed to encode front view skin", e); + return SkinData.empty(); + } finally { + frontView.close(); + if (tempFile != null) { + try { + Files.deleteIfExists(tempFile); + } catch (IOException e) { + LOGGER.debug("Failed to delete temp skin file", e); + } + } + } + } + return SkinData.empty(); + } + + private static Optional encodeFrontView(NativeImage skin) { + NativeImage front = buildFrontView(skin); + return Optional.of(front); + } + + private static Optional encodeResourceTexture(MinecraftClient client, ResourceTexture texture) { + Identifier location = getResourceTextureLocation(texture); + if (location == null) { + return Optional.empty(); + } + Object resourceManager = client.getResourceManager(); + Object textureData = null; + try { + Class resourceManagerClass = Class.forName("net.minecraft.resource.ResourceManager"); + Class textureDataClass = Class.forName("net.minecraft.client.texture.ResourceTexture$TextureData"); + var load = textureDataClass.getDeclaredMethod("load", resourceManagerClass, Identifier.class); + load.setAccessible(true); + textureData = load.invoke(null, resourceManager, location); + var getImage = textureDataClass.getDeclaredMethod("getImage"); + getImage.setAccessible(true); + NativeImage image = (NativeImage) getImage.invoke(textureData); + return encodeFrontView(image); + } catch (Exception e) { + LOGGER.debug("Failed to read resource texture {}", location, e); + return Optional.empty(); + } finally { + if (textureData != null) { + try { + var close = textureData.getClass().getDeclaredMethod("close"); + close.setAccessible(true); + close.invoke(textureData); + } catch (Exception e) { + LOGGER.debug("Failed to close resource texture data", e); + } + } + } + } + + private static Identifier getResourceTextureLocation(ResourceTexture texture) { + try { + var field = ResourceTexture.class.getDeclaredField("location"); + field.setAccessible(true); + return (Identifier) field.get(texture); + } catch (ReflectiveOperationException e) { + LOGGER.debug("Failed to access ResourceTexture location", e); + return null; + } + } + + private static NativeImage buildFrontView(NativeImage skin) { + int skinWidth = skin.getWidth(); + int skinHeight = skin.getHeight(); + boolean hasSecondLayer = skinHeight >= 64 && skinWidth >= 64; + NativeImage front = new NativeImage(16, 32, true); + + // Head + blit(skin, 8, 8, 8, 8, front, 4, 0); + if (hasSecondLayer) { + blitAlpha(skin, 40, 8, 8, 8, front, 4, 0); + } + + // Body + blit(skin, 20, 20, 8, 12, front, 4, 8); + if (hasSecondLayer) { + blitAlpha(skin, 20, 36, 8, 12, front, 4, 8); + } + + // Right arm + blit(skin, 44, 20, 4, 12, front, 0, 8); + if (hasSecondLayer) { + blitAlpha(skin, 44, 36, 4, 12, front, 0, 8); + } + + // Left arm + if (hasSecondLayer) { + blit(skin, 36, 52, 4, 12, front, 12, 8); + blitAlpha(skin, 52, 52, 4, 12, front, 12, 8); + } else { + blit(skin, 44, 20, 4, 12, front, 12, 8); + } + + // Right leg + blit(skin, 4, 20, 4, 12, front, 4, 20); + if (hasSecondLayer) { + blitAlpha(skin, 4, 36, 4, 12, front, 4, 20); + } + + // Left leg + if (hasSecondLayer) { + blit(skin, 20, 52, 4, 12, front, 8, 20); + blitAlpha(skin, 4, 52, 4, 12, front, 8, 20); + } else { + blit(skin, 4, 20, 4, 12, front, 8, 20); + } + + return front; + } + + private static void blit(NativeImage src, int sx, int sy, int w, int h, NativeImage dst, int dx, int dy) { + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + int color = src.getColorArgb(sx + x, sy + y); + dst.setColorArgb(dx + x, dy + y, color); + } + } + } + + private static void blitAlpha(NativeImage src, int sx, int sy, int w, int h, NativeImage dst, int dx, int dy) { + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + int color = src.getColorArgb(sx + x, sy + y); + int alpha = (color >>> 24) & 0xFF; + if (alpha == 0) { + continue; + } + dst.setColorArgb(dx + x, dy + y, color); + } + } + } + + public record Snapshot( + String mode, + double health, + double maxHealth, + int skinWidth, + int skinHeight, + String skinBase64 + ) { + public static Snapshot defaultSnapshot() { + return new Snapshot("survival", 0.0, 0.0, 0, 0, ""); + } + } + + private record SkinData(int width, int height, String base64) { + public static SkinData empty() { + return new SkinData(0, 0, ""); + } + } +} diff --git a/feature/mc_mod/src/client/java/com/robcholz/lumen/client/LumenClient.java b/feature/mc_mod/src/client/java/com/robcholz/lumen/client/LumenClient.java new file mode 100644 index 0000000..9f784dc --- /dev/null +++ b/feature/mc_mod/src/client/java/com/robcholz/lumen/client/LumenClient.java @@ -0,0 +1,21 @@ +package com.robcholz.lumen.client; + +import com.robcholz.lumen.LumenSyncServer; +import net.fabricmc.api.ClientModInitializer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class LumenClient implements ClientModInitializer { + public static final String MOD_ID = "lumen"; + public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); + + @Override + public void onInitializeClient() { + try { + LumenSyncServer.start(); + } catch (Exception e) { + LOGGER.error("Failed to start Lumen sync HTTP server", e); + } + LOGGER.info("Lumen client ready"); + } +} diff --git a/feature/mc_mod/src/main/resources/assets/lumen/icon.png b/feature/mc_mod/src/main/resources/assets/lumen/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..76f848a66eed620dcaa29e1b5497659992fbf189 GIT binary patch literal 1689 zcmah}YgAKL7QR3T5C|fAc{->|krKj%AgS_L5XvJ!NOjDQz(w)TBgY6k-^$ z1cdNl`#jF(^*>Gk<&5I%}W(?Q_=N z``h0-jRg!U9v6ZG005tpolyh;F2t?v>E=WjUA0uF;TGhn6ORCFZY}|Ts|veFagTKcOLy&|@i*4_jUz$q=wWzi7`AiJ+Z@yZvD6T217cm`Bz;#A zgcZkXr0+j{+vtt+=nl4>9!Th<$3+NvHS6=APU9kZoZO7TaBEYO>h_(iz9wCxV??_) z0?%CjvSIE{C2K=kdv);F6Ze~R$w&0m7}S`3gje@^53}BK1?Z{qGV8}bf<9}<~xwy8` zxJjX-(fzvj|E8<*-KEU3%=?z!(v_k$$gTAMl;6_1I8QHA%WjB?rn1FCcU$ezTAy>p zHzHALuqHanE#=6;9k8tbHu25R)4NO#cUFQXTHIgUv=;wdB`Hl1z!LSiI=Gwgxh1b~ z=}z^}6L980TQEV;a{65gD^P!f=kin`O zjgYIdkgK5ycdzFXF3FIh)eobWwmdYsm_Ha~I|F=lqpJb755)D(gih0?f0}ZXG?8$>G_Nb3% zNM}9n@`dsF?Yxt!_muUS{K2J|Ry;BW6jW#&NXvrb;TLxZ%$1T>^$e1l9nUT&gwaM) zr@-3f?7mKw$n$sKSl${YekxcgNtSn8F<{HMpaQMrUDJ$}OqMfSfMNw%`YXOAnsjDp z@Yn?lqF2r4_P3h&iv1_vQTlLJMuC9Qawa)}0C<~M z<~BfmfZg%qvMNQ*tRRZiMkK`$K?(0KfLbLJvoyh*(jX2xG-P1R>^vDMIi+EYm%V!O z@@c#I0dwkLwql?f65+m?MIyBb7WBLCLw>=qAQt#wGwbbgsWDhVDTrhO-X^iCnm7e^ zEtpkjiS13@=7F!o3yH7KZH{%`L-#(as$Q?Rb|r$B$aIL^rCo&!M?T=rdzy~Nv!w)A zI7FSoZiWKsma-KD06&*?c(n|C$Xx6NwEh8)3^<7U@%b^GfPQUZRaGS3DoEli=;5MS zwLUG<+c%+S#aAQ~Ayj$PCw;FuLJk^a<6c41_~k003!#P8p&rr#1V5U<4h2#!T*M-M zEv3zmIo!jv(n6$GXj%L77v^-gHf7rbk0K7G%m1*zW6sr=6vXQn!V}<1Akdp%bgN>m5|aqDR>1R4tQiK zI`333!;zB8BlvmG4;zp(Xi}Oxmt-sE=MXq9XNc{3CkfS1J?@@a3mtl5ZgaY8m1v>M zDQBq18S4AH@TX}kum|HBlzw@x4))m;U=7k~rDUiEeB0NV%PvsR!2kc>qknao=o*f( R^%`|sSWYG*qwhVg_r`y~JX literal 0 HcmV?d00001 diff --git a/feature/mc_mod/src/main/resources/fabric.mod.json b/feature/mc_mod/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..58f5781 --- /dev/null +++ b/feature/mc_mod/src/main/resources/fabric.mod.json @@ -0,0 +1,28 @@ +{ + "schemaVersion": 1, + "id": "lumen", + "version": "${version}", + "name": "Lumen", + "description": "Support client for Project Lumen", + "authors": [ + "robcholz" + ], + "contact": { + "homepage": "https://github.com/robcholz/Lumen", + "issues": "https://github.com/robcholz/Lumen/issues" + }, + "license": "GPL-3.0", + "icon": "assets/lumen/icon.png", + "environment": "client", + "entrypoints": { + "client": [ + "com.robcholz.lumen.client.LumenClient" + ] + }, + "mixins": [], + "depends": { + "fabricloader": ">=${loader_version}", + "fabric": "*", + "minecraft": "${minecraft_version}" + } +} From ddaca980fc58454abf99c19d14d63b0247dc3a29 Mon Sep 17 00:00:00 2001 From: robcholz <84130577+robcholz@users.noreply.github.com> Date: Tue, 23 Dec 2025 20:23:09 -0500 Subject: [PATCH 2/5] feat: lumen side is complete --- CMakeLists.txt | 3 + assets/icon_minecraft_sync.png | Bin 0 -> 830 bytes components/vision-ui/libvision_ui.a | Bin 741944 -> 744724 bytes components/vision-ui/vision_ui_lib.h | 16 +- dependencies.lock | 33 +- main/CMakeLists.txt | 1 + main/idf_component.yml | 1 + main/include/display.hpp | 11 + main/include/serial_pack.hpp | 38 ++ main/src/serial_pack.cpp | 293 +++++++++++ main/src/ui_assets_provider.cpp | 274 ++++++++++ main/src/ui_hardware_driver.cpp | 87 +++- script/display_skin_payload.py | 59 +++ script/serial_pack_send.py | 54 ++ script/skin_payload.bin | Bin 0 -> 14404 bytes sdkconfig | 718 +-------------------------- 16 files changed, 849 insertions(+), 739 deletions(-) create mode 100644 assets/icon_minecraft_sync.png create mode 100644 main/include/serial_pack.hpp create mode 100644 main/src/serial_pack.cpp create mode 100644 script/display_skin_payload.py create mode 100644 script/serial_pack_send.py create mode 100644 script/skin_payload.bin diff --git a/CMakeLists.txt b/CMakeLists.txt index 108ced6..cd4a344 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,6 +3,9 @@ cmake_minimum_required(VERSION 3.16) include($ENV{IDF_PATH}/tools/cmake/project.cmake) + +idf_build_set_property(MINIMAL_BUILD ON) + project(Lumen) set(${IDF_TARGET} esp32c3) diff --git a/assets/icon_minecraft_sync.png b/assets/icon_minecraft_sync.png new file mode 100644 index 0000000000000000000000000000000000000000..1c245c8edd8c93dcc3a639baf4110b5287bed43e GIT binary patch literal 830 zcmeAS@N?(olHy`uVBq!ia0vp^DImal91spYZKRF6^2u$g_t?Q)3;>Hl>XQkql zz>@HEi8GY@B%Y&DwrE+=viAa>=AOoe>#td@74YO1y!2^jRn~_mB~DAMDpuO@mwnX8 zl~N3n&RCiIx0&q$x4w%NTbR|ie;Mu(ib3%cg6`L(cOK%LcDtGdmJ7s==9HO z&%=A~zwe*+ch>jn{cI1JGZfigSsc3X-n#FJ#8162kNl2v;YlhFuI=666vh8Z^R+Td znh_3vp?Ooq>6_q$E%}L$wO-EI)0>*3=U(2iByayS&%Z^vm);mOiUplh-^(9d_5bds z|0lGaI_fQM6=!%>PqV$|aqDhu_4j%eyPw>ON77A1+TUj@UUl^T`t$jnd*?seTzmQN zt%-!)-8t0?KGdbg;?DF?MFMMA9W1g6W;@XBKf0gFU);hG+a+&xe=b9iFjx%X3 zE*w1!Yvz=Av>f0#aLThC!p&+wko8ymsSAfsF?Iz|^*a|hgJRGHRX3VpK<*sp2f>?` zIVY$oV}u1t3Rt3*@+YOGCq3@2q0_30WBzU@DxO`HRbJASj!Rf2jN|YBsJTnIw2!dA z(>%IU=3I5@m2d9cg%8~of7P}ZpB1Q3Y|B`{cYpg4>-Srp*eAagaXNL7cZZ8)_1SsJDWf+@2oV$gxx4->qx+ ofrC_)WO~fQ6}wo^0Yw-*UHx3vIVCg!02oSZA^-pY literal 0 HcmV?d00001 diff --git a/components/vision-ui/libvision_ui.a b/components/vision-ui/libvision_ui.a index 5d9331a6d21145728881303d0cf06e84f4e66a6c..77d731b2a6f2a16d7bfecccfbc684251ba9a2663 100644 GIT binary patch delta 151078 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 Izm3NK06A|e_W%F@ delta 148227 zcmb^43%pEa|NsAM_G$KZ*g1_Dw(}{cBq41=nS(!nS!XnF)pBl|67@1FtXkV{Ui61b$u7&Iy%P`Ll~KE1 z*&(&-OdL|X*6bm*Yt+1|cGJ@a*KXWlaP5W{4X)kb%E7hk-8eXDjQMhcF_Y_dsE}DV zQlV7|W7g&wq;0G5=L&{SM70 z*PqyIa=l2iHrw0n$gCeZZF2n%r?uSHX4m9;FE`7q|MF?=c2BM!Y2I>Yn>~~3O+1Yf z&D(uIndZ5TRwYY2mkftx-WNL>y;{D=1$8oWrK20Pbr@r=@RCf*xlx{EzF&k+_o}W5SgAV zd^Q?S?szU*qA_(|5a#XBD^+JnuTJR;b(S>A&8wmn^0x(h;jY}wD#;bkMl03IJo(e? z+{$fU^K06EN%M1CWgD}9+9{p)PA#9E?Ujuse|a`qvR!7Cno&PvZe((mCjw=jJTZ4^ zB&=rBxqda}bK2*G6;&v8S6RP?rhSsLRz@o%x37wpEM3`;X4dOaCWx0P!xri7C$Z)G zB{!{%)=Rc|Hd-QDerleXJ2iW5%~CP4P3#wDd-+N)+v#@m+MV)Yszn|ynsRM#t zxPNNb;Pu{Q|L3D8H{T-?8nPImu$WvdQ;@Ny6FJ$>m5C)wZ;YPS?4V7EvF-D+mPd>!WB)0H zUVTxMk(*`vl1%W3+ny-9#(cGLdeP z*fVSEY>IvpX|Rro$g8n@I&FBQ%u-_6Y$-qcwS@ye8rlztf9ll*3Lv`WTvrINWXM{8&7DV1#U zawGy^QyN`q927dCaTT?+hFxvXT z%{PTPd-}l>>72G_SNp*O<5D?e1ShA08Dm+cep$0u-j9&Q9a>?yh9wjc7dQ9_qQJT=KCuqKymfVRla{Q^r(j{Bggw2k!`Ldox{| zU7xJk$GqASmr=v#sRUg;Tk(l>#beVIcPCc5e0m=pOK)!*tmOwM+?;C723z^T8R=jL zg5CV!fpjoYj_A}>G}S(x%b3RQxIEIPW~6_{oxB{7J$u7V^D=5iuE?5w;O6u$vORn# zRgm2mw!6Y_;z8-mD}Hd11V12{@P_hA>Ed^O))VQV?O}Z;PI5Xu%*Y+`S6%#7yLeWr zWY9&s6?&v%nX_k&PNX`1lqvUSr;SK6HbU0y2S>{a-}M)@J>u84baYtR%YR+jYOU-& z@-P4`?65sKmKwIHad=?7H*B-*Ks+z`=o`@{-Z<=^%EZn~6|}XD zO2sl~eYO^wCKQ)?a_EltjhP_ttmdJt#mkeHpX7=AyhK-TZlA~O${YG+h_ zAmUfWl=gm|6n^%Ck-8bDy~347gRj$Jo7mt7!6|-$UsDBcjFe4f_DKdR za*vKw@U9lNGApnIt+&pwj5N9$?GYi1MamC@``TVPoOgOH zzkM`#sTELJxHN&WtrOW*0B2)0lUW_wM-AkXuKvmQ4-j z1N*|8jmr`Dk{{{C-r z`%_fDUB|G#i>AgXu-}Y`$4qQrS8u0)^VS|AByd+Z2Ii&t|>jve-Q1T@YkWF?Q0|%DOtVF-}OH@p6Lhq|8`KbCN`5X z%wVY{;v7|{XVpz-@$LrLOp%8p4bGtp-r9Pbz;2KyK8#kXl-?kwrI&Adq|7m!e{P-R z+aE^jXRu*^{xI4(gWMhW@cfkME6MVE^0cqv&w^-{(Gx*0KM6|D))4 ziP24Lb=)hm|3peevOhe94RCKdYJ&|PQY=zT5)`gbtXI6I*sVC6Q2n6DXlffzEo~Qc zNWI|QWon6Rt#U~N`nEisV51GzDz*r`t!WQ5)6Spry-;LFB7Uc>=6^oLot>ZV$IWb) z!jq&^?JK8I7?)^a9Ys@t*J3aMq+f8(2&RkR%EFG?8g`BB-JbHaYX|r7Ds@w%BFGlZ zD$N*SUhVzK@4fizJl=s{NFM6TxQBl)&q!|GAFYtEmwv^|1r^-mS831LUZ-qpYy3`C zsv8F1N(EC_I-5JBYV@uuy>j+U{%tBtlgmDhHp*ypPV&P~qmAP)G)~_HKG}3&q?*V< z?_OhMM3ZFogVFUFXEjZJbTGOnBd>k(`Ol&^CZ^;W)7{HBqC{g}XU=|be99x!emw2# z(_WhP&1tVpdvx0C(jJrc=Cl)OPf4WepJ3hFHkb}fOlMe@_VluO?ftvgsUkD}6uI|L zky&Lcw_khW^Ju?FVkyP#;>}KxYpd5g-Ixpi**V{!$q6Hff4IN`tKXwu~+;tSsTex&fF&dh||bYTM&V`>MqIwc8es z2y!rU-Kv2f?Jn^kR&ibnQ9B-e~O?Np}A_S~fB<+5YS3$;s(& zL`x*X^}EGpkKCAD?+mvSTKnI0Li-x5-Z)#UUF_=pY0C}n(B4b*{`s4#wFl1nCEFj4 zR;KI?u^_1VFIFO)LjGpl{>@6H=L7p0OutC$ z@cN4n+x*?LC}@9D_RKS#HA@s98~=}kY)Me{U#yy+_P4ty-ag0{USFlAy5yTjWdA(l z=Zj3vzPgRG7XQCiVtG*WU#!Ic&i=8NK>za&S{-y?YF>x4imw{Cmj3_D0&9Y*|6<+z zw7=UyRsLq|B`1E*4)QNt><8YQ7gkoe%dW!z>)XSNZ*QOKPARV%ud3&=57?^iPN&#d zgSm=FK(K?nL9>r5dV7AHYL5r|;g$(1ZX8y;F}rKw|FL4qdW+VZGbR0opkL19^sD5< z|B0S7&|V{5L7m<;(zfxYhAnEZk#6>b`_e&siFCIg{677b#eUH5%}&8RDf_~!#Gdg# zFHFuj8ZAiH=bs!KsMpp8swc1U1J`(gwqD}>ZIf>ujn*p)-d))Df6uk`I_JOtvu{}5 zNrzh6P367+(oe^1j^k~U{r(%Rnqyy|+|GMF+`c}&U8*ul`X94(|lb51Z0N}ZiN zpV#3Fna3Aye{#RC{?&k0^GSv)mp``Jd%jiKH_&FxN+C* zM6^=+HnZ2a;6_WZ=l)q_J89od`%7ej-Mt_61I=T6$X#eE*KSisoE%-TlXLe@Inj_}EU9(zd-FqW*udG!*yJ4e7_eF@r=I*SK zm(grwa^kp*^4a|^zkF6I`bUkt?7>5Z%=UDDlia+_kuxJ+rWfYq_HGev%}~iQN0Vny z%_vNc7@J#RZM!pa2Q*9$yfXLf2DWU(x;I6=A5j#+4FMlk3Nu_%*$+Su%%@SuZfYo{r1DKfX? z%VZ6v;5IxI=BLlVC!|C3@c!V_{vxvguazyd2v?I1C6*DGC>5^8|CS0j;2nN@?9;6{ z_0*^h7MUIRN@?(2yi^+4hrcf6S5Ra=$B*&<)KZi%--7y5!7<#^Pp5%j@xNpjWkvbi zSoU!#9Fg=2_;;h3nu0!4^#vnZ^XBN)KD$Ph_7LV$SL5rN|WFQBq+y+#wdG z_rgU|{&M_t`7nJ5{>|@jf;GI3z$LN;ZpNi$7u=3#`vu591y}X!!!vLxS%G=Dqm*BO zZ;}oz!q=tSWBeDHWdue9AKDd})%Zg32K+BsrZ5h)xmjQeYSlv zG$v3+)~E$8DaUCc?i>sn#sDrS9qNX2rF<{EQMTaaxRi8o2<~VPORs%8VE4yF>EX@z zA!+b-JWVQ`f~$pxk(q(_$|j$OPnU^k0d6Yg7vU*%Ak{t{SVmxhbYM09PzKco{H*Mv zt$3uY(GJ`~I`l3M+N1t`c(`=vb38fUZ=Vd`5_lpgQDlzc?`0zS6`zm>vgpi1GP9M! zJ7pJCz;W3kRdE;UNG(OCE{>E-ufH7wO$i*8L1n+P@Qw_^_PB?npNIR)7V3$cNdA6! z82|Sh9K!1c3}oLUaJPgM7(?JqIc^g;OMDkDCu73S3FpWf&Bu33eNW=uG7~PtpU5tI z0VnR4HP}SpYuTi)eD%N8h! z3&a(0%Ww-POaXy!D+yzniao*n~?{~~joz&BFhcYMAKswkUx zsJIMH$S$dj$H^9|g&&h$*a-KP6>5PGrnZ=!e~V080<{@Psinwt#to!H7vjazflKkD zvc^|pdsoXZKO7&D4&H>f$S%4a_wcs3H~$xT*MFM{`W4*E>#t>#CGl-CCLYJD#Q(t6 zWQ|wh%5qe^h?h%!Tk*C0KRExtX){QV?YBT~lTrK${#3@mSGcTfk^kU{;-7JbbSRV8 zmx)X9x;yoG>u+~~{US?C8MSt8YRf3Ej~|s?)C~V5>1X0DGI4dp&&w`6AMcQfrxzZk z=l}i$sz`>Rc$2Kr^|-toC1Y&`vPCB1?|M!xBz1SEgehrTj@5GbDd+-eLejHv!7nnnMiKHLFN5wxR2>dF6-*Jgx zixij~X0=MnpaP+%(JmXcl#w-eXHUBylD5OJa|hQf`^G$ z;4y&{1!fI_DM6sXY{K)zui=&Aop^(I4}ML&AMX_(!iU61@b|vC{xLrg$O%p+1?G2L zPHeAmtBLb*p|~O*B(8=>itFKV;->h)v>E>erX_(*lA#^mF7Ax?h%dkg#eMJ*@c{g@ zcqq;ZX0ie^630`v^KXF}L!g0V7>`?sr{ON*dvP!E19+JDQGBy_3HH9z>m6y#19*m) z?#=&<0Rj(7flYX!_%*yzyc2H_@4>H$_v3fPhwuUM5&W&S*S;MC1WrhX-*JYV8*-Sp zD~a=Q9dSk6R9p=giRg=oNC{W0Rl55Lp%Hrf0S}II~Rvbd2<2o5d_Mc-j4g@ z=5hfuv{?C=8G$$F`t9@eo0|!oCJo+!4+aesnyGk)cqV>au9zOc7Y7qfp?MUy5--8o za!K|qzA7WV{v1$dErDO8z)SdEIoWK(7X}w3g=QE2BiLev<^z09(4j(ez~&c!h4)E) z|G~G|_xP!$(ELQ;N^yi!?=0qm)KX+hVEYa@wG?{y0`^M&IO&raH>st_)W&CVDW6&j zO(Pr?pN*xPB*7|&9R6NB z8B2W$b5HSrnTw~%8a(XyN!(S|_!-<+{5&2aei7dww%_8O6ga^?CNL)m#LPSR1Mx>V zKQNzQ;4)mM`CIH;Tta*dACN))8*U-#b`ompJHfp#At}Z))bW^KX&4pFnu8x6s&cqaTn87ULRn+ieAI70irDxp*ahIyha&%sRYYyczEjZ^OC4^+n9=!pnm8*##dE*eVSiz~QG> zh2EX+y^{Vt>GquOckqN)p>M!|Jtpb7_^7xHE|CZ}QK9z(1i4~6;=2a(K+H72H6^_n zE)ciIXNm2N>Q>?|SnKP76ZRbLufe4RE)oyK*NcbYYsEKUt#B;vDIK1Gm&)n*E_}C? zpIwpdAKrQ`H1o;uq-1y!KPF!0RJa;%lyv)%+dsvx;rGRF;djIzRAl^V4-b%GrS$kK zY(Jb#Eiv;w{*P4f6aG$|aWdBv%=f9K(3HecaRsdPRZS46D;etIn&PJTY;h}mmbg9E z3cEP&f$gWMsU>DEb(|PTpoW}ohvCblfg5pevHkIfo5Yjx_2L;=>zjvXO8O)C?$8Od zguqi_z&wlXIX<-%nsrz!+>9GYhqvK6es&JyT{!%>p2HdslJo<3R$$KmUlCXo1lY&8 zj!YaUa6OqgOeJo)h;wmxLm_6$;AxUx2|q5bftUGC(BlRK_WJ?c3}u*;Fo0= zO~V_-GjVt)G-~d5{0N?sYQWC_QL~u97gE78{F(T9{EK)!{z?2Y9uxF9YTj`CHcmI_ zwf7;e%_%y4{^PwGftp^x1`ExX_)PH;+)8{LYlFWz&gT2O<84*m5;dhASHRx1kvjiH zO`HHfoRMClrUtI%lPy#aHx)O=J;lxO0C8&^j`FBE8|yCYjGHiKQtfjIb|HaqDQo)R z^+BMl>5s3JEpQdSLOcT7x7ewr$c(|aO8R&_T09MNnU`*#O@1$dha|%Tc(eFXyg|GK zxAfaHWz7owsid#LpNKc%AH}cX;}!h&>A+3`THzjCIyfr0Cd4Jhhj4B25nNOJ1J?R} zcbvoiZWpxA8syu6tWibWLtM>qJ$$L8H+9?+hlgQVzUo}uopEB6RB!yZh@-RJN9wD$W44B1uah6|Uky(K&OM`3i0m=Uo-Y0$?KQCio7rtM%z=zmgs;8Dj zq4|_R_=rWJ`5K=k6&%I)$r$+sUm&ACi)Zmps_0i(XiDLd;tIIAbf5q?@YC)4|GEU~ z_yODmKNq;jw8ZvWKD88@cDPX78Hb;sFqLC_DevdM1oskOfybmf#Q5hpCSb4S{Q@`O z>%?Pmc5oZ6$V|j{OZwe-s(3bjNVed79KO3QGEd?qsr+{SD>Tm#u)l=nci?$kNh;WY zcS`zJyj{El50p*(9=1P`=GXTz{!IJ@j(A6rH~$uyBLo`z`T3#MA8ZD$`}_)i$G=Ji zIR)%PNzcc%#TD^|;%d07xE`Kh?X}MqXiC6dYWoeg#IK3l;g`jo@z>%D@aN(_xFQ!N zetiRQS@BTZ5~tdy10xBvkPKt+`Qq{TJn=Mqjrd+XSo{EfQ2Z#KBVK~p;_3F;9gMaD+e`Kb;1T z<0azXaA&C?yBg2^ic8~R;usz(F2K{pb@8Nx1ey?dU3><9S=<&M6?ekli@W0}C!o|) zXnNypaetfNPi6&&;6%mXx<23gS*;r4QDkT+z7@9+Pr%`a&H3g|+(pvw#XZIM44KM{p(CC2MdE@h03LFwbASMxa>` z$TvH2co!?*?7?FseLwa;KIcu9IfPeB`VnlO4@)gY=0_Ypx>Mv0h{ya(IQ#rxUJd5* z;Qe`#DThz@Gmu^dUnyt9I{4~f3ly0q_+h!kYKfl^pN)@7{ax^-=?j>IJ^x=spubeu z7e5?aA{CjzI4ZslH9)o9!6L_8YF1$!Q8z*Ir=i^+ilv7Kg zc@k%dm*HEp{PyX<3j}IQhE2GN_;q}qco*&<{t(|GK8SA=e}h+vkKrX%((7+8LVqLh znPkYR#i!filW-o_f&Lm*!Vz&z+|^HJg&N{Ul72d_FD}F<+wXs+mLk)MKu5`N0k+rr zsin|dg6GOSFc9~b^lNZ$@hE&t#4lfDZo~E#ern-+L^$z?6qrfiA@Kt^{9LcdJcd_G z`ck}7{2UHH9WOHL@mrGq3VvO@qZa$WiuB0-R$xgkvr-GUTyRwUB@Q=n-25BYkn|I{ znmAIMAEy?Vz^9AL*Jl5Bmq1lAbP?CZ*NdCtYsIbbed6|bruaPkoVX`mF7B5g@T~-f z;4j1@a8)iM{4FvD+Y3wIw)~`EAIHs|_N}k1}_$`#*c|N z;(MjRSMf^moA`anzX#jPF24hx;Ih>NC(Ks_?60i)8UBOYiGRjtiZkoz1Bh``5_b%y z=C~=3!z-$|se&)}tLGkTZ9Ft^B5oQHm=FZw=5#zyd=_3LJ_oN8pO3eTFUEVt{qQ02 zU~K#?mY_%15-8&b@J+a?_%<9~cE`i+!54_<;sN4^ad?#-H&5c*)9DF&+xZy+ z2c^LCwt#H%4S1;dRXj($6VDLa-_W#IRjGx$>v+BRYn)TVOHJ@^p*c#xqynCQ!A-^X z%k>S!rSW0uaLjQ5E|Wo3-V!%;anBlQ(q2>C!?SIlKq~^nyg&0X6I%}_eI*=gntE|zTSl8e_Tt`NE(n)^|e;^Gk#k$JdF!#P*z@S_;kgc%JwtTuF9Gq@L$=`wYT70&OHi zS!}NX{0ifEgSa++SKJuy7N3E?6}Q7*iOxi$l z>C)kwu&%&uI74>XWGDR|+&Li?&LhxH{0JT?UV?{;pT&#C>##j#raRxGVlld@&BcjvqJuu&%&h$JZ7!=l`1sERsQS8!pLg<#%Wb{zDp^iQ7r~ z1GtU&G2BPI6x;KKU*B{1df)c`|9S$~`T;u?V>|!*1$N-c;`gww;m3}@z`8=;;U}g1 z57?eB?85jjG=C6yQ8MJ7!jDFX%i@p3ar~CJHqPhRPc4O}F)krK1J@U~!*=>+vGaeS zIhVjJ*##G3U4u*TGD*L}@zr>zq+gHi{GM70%~*W4cp|>evv>X}G}8$TlMHk56!8K) zLA)40EM9?=;a|N9;Il0bP5Mc?1!lf*ya)5RH$IIP4a@lbIE zJV;!C6XJT-cDC~yJe9x)vI|;aU4ye7pX>NS96qQNH<#cirT&39DZZu=<8O}y?C)Fe z7T<=Ch^OGg;+Z(gNcUIZ0h}p*3|AK~#Z|=5HDdqk8vc_E>X)&u(Hru?FcS%M3Ik?3?jW9v z`->Og%fySZuF-PG_LtOjg*M__gYta-Z+}NYecUVC?{M6limNbg{1s@8%ZoeU4&tu( z%(NN*h2~-cH%f-fF~??5;Z=BmG&llJ5!*i*p}qq@An8-_BT}FJV-gRgZ0BE|{U)$l zGCYn~ivNNCC0>PJ7r%(L!B=qjJs|G?JAMZb%CYO`&Hr)p5rKnJ;pce2_&fZQ*#2RP zCi&G8S9C2g+QiOMQ_Sz>2r7vNO;Gc_x zAs9EaarhZg+&qN&K0&&DGCV;b{7z@w`~!#I>5QA_aQL0hxcMi(OB#F`FA%?h*NET7 zyTl*jgIRw2bl_70-%EzC@$cgQ;07|Pf5L_0$f?|%7U$tH;<8welFE2#`2Byz0D;v~ z;1s-Bd>Y;+J`?W|cfbe5=i$TRi}3H_%W$HccNp0H#uy;bSbPmG6yJ#Tu(}m5ko1W- z{KczzvPsE-SJy^jMVpjG4uV~{R9?BhC_~z;6;-D zgX7Qwk&>nc4uAiZXF&0@vc^sEcj7bfm*PTP zmlINI;cL9uUPJiqhObPU@n2|q5x7J$T#la>55eJ|c`7v5;m;)fX8f`Ec3hF!(Qj}H zE+d|SFHG6azlCNVfliWP0bU?pgy)EtVLM&>4XnnWh&SK}hpL~x75^sbJ8*q3-JAal z&ASAuOM!j3hF_jt^cn6UK8$Y{{|Da<&#|nC`0Z2R5dveS zz+xPJ##3OHzFq2j6R(i;_wbs`^!nQ#e@tM46!-!U6n}?< zF~qlKj^k$j2ExDL++Yo3Ci`^GcH+`F++r~k!?VKkf6NpR*bp=rGj;J^aT9!daJr3| zGw>8~TTCC)OU!h#mJW2s;p2EQ(;FvlkPQ88hT!ZGGgska-`~f>q`}d6toRN*Lp;@% z4+d?_%)}l2{;|dGKb`&GSptuc;R5ku+*7<9myiy>fJ=!t;(YO|IDEh{X5Pf%C#x~@ z-s$ZB@RQG&`IrpfO9fxxqvG#ycz$IJ;1j{1VhrG4#n~;$FT1cbt}2ct2z(%c0^CM6 zVO?BLHenOoP<#enCR?a2&X5jw!a3sZ_(9pliQWW0mq348Kss<0t||?Vz!}ov(Rht? z;0}C9JQY`v`e)+L!}1ApKY=Y$;1L{N1;@-{ykF9n<3X}UFW^OCg=QljE-UaV9wmMg z9}DvH{lE7JWJrdO@m^WuFYwUdnl5I(!;ee)alBsq80ZDIy!?#k*1GsN6Gsa9?JV92d6K*Z(-Le19iSf@wK%hjhNn)ly zE+xJSPZE#7;X9p}8I8M1`W^TJ@l-rWJQEL1+0MUA1O%>=43FS&?q&?&`Lacp z;2*>r@i^H6uj1+AH*xBxSiSk5F+dBraXZa!6@hZfOxpL1|B1>kHe*`ITeSYvZfUXFIdW&v+=z_fwJaY zyauP*=gVgo5(pbCYc9caq`(z;lK5&oUwl2@6zqb0GX`%7wqU*)kHeWW-%Q0k;E-nqxmv0v0agzUOd{Dd!hZ9@AS&zdT9QkGoo)iB5Prlhs zV4GC18;7?{^37hnPS)rk4z9Xcfv@qQV2k9N?{Rp?glEI?2U6btU9CR;`g7hgQ39K# zf_xm_g2^`(@gzwvzz>M);5D)f8d*zvbG%B@&%}xFNu_+#p1=^va4rr{w>%q;@0Il4 z_$z7PavYwH^UYwqQ1TDQEyXvsV*d}6z*sUoC{EzQpaDK!$K!)d7&rIfaHfl!Bo5Dx zaq}2%8cfY`^Az48<)3ZE_TMRiwPZLdehG)yY;p5C&JU*2xOodV6~B+$h(Eyt#9!jB z;(yyU>?VO9@f`6Vc)U2;nl%=mgu}C0+?@yJ#i|5@N^kBt#D2- z701ol_=aGni<@(C1xddUho@IQ^TFdJ{R+HHd^HZQFZllD^#pcEhFkDRX)uAW6W@s| ziSNVB#Yx;w{1_f8ehS|ej43|?QS{%-7aq|+MAnC8;@REx$fWvFZxOpFM z3L1!;Pw*D;mw1w2pMC!C-vny-0sJEl50kk012>fP=$TAB;*)TA6fp*HA4#u{r-?_>Q?>JwZ~(!a#L?4bABw_||7y>b-%h{Gk%{Ep*6AkSpCVU5M5 za5r&z%=sq0 zd>kHTB~4EpcDSUu47Ur~r-6Y4MoI-&<4NL?xJW!24;0^y2Z<-+a5>Y=F6J`kK^$Ix z@aN7SE0*D8vjkTPYB<>}$9leh0qYgdMy#jnS8;Hi&*}Y5yd#FTGhII&qOEFf@5To->OZsPb19NzKB zH*K+AFLc7Xg}P&{zxP>;KW(5t8Pr!{ZD0h}21et@gNZ8N+=2CiV=8`H(r4muDQoUO zE8PCX%bG{XP%+3+)-1-=#LID0@e4TIr)AAXr~Io<`8S>N?y6MIvJW>l!z~Ur7UJ;P8`EZb0B) zCA||4uPI}uJ1&*lW%l^v1_S}!hy8J|iAcW+>lPY;bqkHgx`pn*x`n1<-9j^QY72SC zKVyJEc$v%?!2Tw(9bAlc3oXaGgwz+jcY0 zPH@jBCq3pEg!hwSxA+mKg2h;WA8lS$xKM=e#;sc2UfyX8A9@d-B zA7j1w`~{w$8+Q0R{HXXiULpPs+dt)%S{MWE>99Cant*m7hV^cC0p1`Ttczb0H*w0J zftN~pTf9!(2}}L_{U-u?ySz7kKzi68>l$8#mrD8wthZiA<19(P1LumT;&@=r|1$~b zz1sV+-m85Cw+{+%F2G&I%boHsVEuu@jkvYse-#Jy+w=dM1UegkiJSLu7oS|+eT=(` zzrcFK;XAB%xsKxtqJ9CsmV-wI^FrKYci^pC+6jC4Sek(Ug+S{Vo{-L9U4Vl-7Pu~6 zD(Ovd3u*8StoIYzV*iGNt*;YKe=xeyrc+dIYEMSa|stW36zxlm7)gB$#UBW+M*giMV+cd-Zwk(}6b$ zgr{4^01i)|i~;Q3FtGh4{}(ts8}ijG94`E!TgN{ZbGG?4?Ns|F+eA8s#qCFyIXF0* z;*z*jkR{uc!DYm;V&(Y&!*LDA^>B&c`oly`V*=%Z3Zteu4&T{CO>4*Pa2?5CPg;_$GInwxQWm`2Sw#}gb+ z!vli$>A(yEgZ%pO930+qiJAu;Kk9gqKsW)WH=be?uJJ$jmd%us^dh{_{*r0#&6z zTg*cY=_St;;g&whf4<|MxV5BThPelkUh>R9$5-QasZ=}v<(ZKL!l^XRjK<*=P@cIR zck}Bb|76G0@uiYJ8;3Jzo_P?Dl=Q~}d-HFeSrP zyYO`J`*@DE*FFvIC-8t|_yW%te~ZJJG0*(h@z0JUor>qr#fzlAldzpRQ|;4%iUh)? zq$zM*$8jUa%^jbK!$Dlqw8yzY10~J5m_eOxpZot85C}(YNz)sLqqd~E9M_Nv20I?^ z_(ohy@{h&!#0lIqXrB&DBhXAT%*4&b^Kf_+lr#_HHj=&&r$;r>lI9<{y`-g9=9dU88|#{OPWG_gH&)19wqLIZx&yKZx>(c z_=?VK|4EWzC>ib&kHE9UH{)>0H@7*S7&&Mnq~gozRe51V|GkNZduV~(pj zu8qSnly4g1aF^zr(;T-hW`6(gYy#ojoNqc~Z*KNFk#D-=dD5X?IGn2UO+Uwj@Dj;? zjpG~C3HJUi1Xf6a@pz?pDqba?f!B!VI-c+N3CBytjQ@P|EP+i@;TpVIywUMiyj{}& zg?EZ~1B@mW}(eCp`XmAr4%|oJI?3<=9wUAPPZux3`YhNpc$TEE z!t=!c#QJF0=9JUz(}C9sJT3)x;)UY(uzr@i&+%uD4?F&k;}dvQ#BZMt{6S!iG?;T< z@lUTyJFeik3J#x|;ip>hPO0w{$IbArgk)$%Ap8y|ztxJv&ja#JC&%4zcy`G*7d!5Y z!~6aD=1RxIaH3RjddfG~69{jc@eC&}C+T-!eMEB#4j;?OH}~KMl79|vCVmLF7C(L- z<4*%mk)f|-SmF2uJVeqr;9=rd9B;=rNc!83KRA#5KT0xuLI(9AJVw&L!{fxq@C@-U zjx)M)lt_9Wo+B=kZ~`YguI9Kden2W{jMs=yciaYVmh=ve6I}>wlMEMP{kh#fj{D>N zl79$3D83edE*^!mGX1$X-`t8Tg-)0W#RKL}$M-tE-|+&+Pda|u@pHw@`F~xp06!v) zYlJ;Cui*ybH*xrN{Cx8+ZY}8_;WpxfxKR8x4nD-P=l|~sjFAjK;<4i29cQ0kJiV0T z@{Z$1TE zKM;?^;ZnwoF6Of4cE^()Pj@^!us8pgH4hS~7_?Bb*g^b6H*(zE@tK%6qv`hPKzjm1Ws{wYhl?+8+}rWxjtApWQhqp|Aifb# z3fiXwV+qWV3<<~6@V$~g6VDOPbNn!VK++fD@Rm+l^AG%}zy5ZRt+W9tuof>AZ*u%9 z4&P{$H9PPc$^Q-xm-1$><4}=MPdJ>FVtk0J zx5Q#5iaQ5u7&G~plb^S+r+7+<0RKxbF;f-v^htVc+*jPt@o9L7q_@Vy#AoA?;?DSn zl?ccP||O~i^SvcGSA+T8Z%RafQ`n? z499aF&v*QUAg;M=wEoXcsCAbrI`89@d3PE@_&i9 zh>xV5YM%}qcQX8nUy}lv7Z!I3$7S($$zKWY6jyg#5AP0VN*;kt1=2-grUl+36`Y0l ziaX-{;`8u9aSwb*e2L=$_;5H=^Ejjz@JqzZb@-@Hw$M%Zn0TDyiTH%1--W~5BQbNI z;|H(_->LF=qzwe`Tw`Vt4nJdynP+hLky*^F!r|?dnE59TZ>Pk}W*pu=iJ8}N_z_#o z?8NeyYIt0dfPRsCAFd-E`V2P^AI43^|8ac6@gGp_Mz~TLxn5lx> zNQ1R-c)uoQPQe`|y_w@yj@$KM{CAcDoyef>hQk{`F>^8QCi(k1z7qG4^kKNC_U0&;tz0m$0}w%!4HJ#33G^mehc|K$HyH1;yB|XuJ6JMO&$*Kc=3m0ad?L- zW=_T{g8ZESs}Wcq1b8kRZxT1go5iQ&t>QL#o45nsCGLWEi!a3K=d|qn7c+eb9Fz?G z@geaLd_;UL{$4x^9~Iw38B2X*2#~=3WU{-;aNn0t;|>ODtxd#NjQl zn0eaqbB@>H@Qzr_yoA%gUSQ8Z{3ZD?;Q38lFN2rf5;O1Omg0|ac!w)y4&pYF{x$9* z{@(GAxTlxyo&RFycLJA6fov{+!dqrBQwk4|^zx45c%-D)bld=kcf4ZeRP5cs^4ez$ zv?MS~Drk%6iHjVcj~|rup7?R`Wq6f%pyR9YYSL5f(}9ucfWOH{ zk86a#R1-7%y+C@*(V;JJ9jV}3$N$9*CH-f|k&Ag>AnCa{yu%eUC*c+e$xx9%YjJ_& zI=H>0H^N2Y=D4f)OvmjVpNkV(-~uN@Z^xHA9_)Cy;~Q~sPmV1*7VCrMiHjM3L!`&k z$e^Byhf4Z9JY4)R)+gH+;&GDx4?JGH5>FGay_oT*flXwXE*W0M_lkGmS>ku_Jn>!} z-t&u@Pw@gt{|YY@|C>!_mIfN) zHsaH8J8|pKcK(lAuvHQ zjK_D0r{c%OGaS!#Jm2vXj+f#^wmz?Yw!pIlmP!L_@YCXrSig(66|a=^f8o{Q-B`by z_aR;{=?CxzoNAvAd`V!lWH^Gih>v6a?%uC>yQF9KDgH&V5_pfKm&NLFV)6O zbpqc@hI;s@xC#DM+ya~I@UT1!XNWsu{qEm+SigAI!*hE5?I66w$uPk2RgSO2>F-;5 zgZL)L;~Yu!3XZEdu7$fv zeWzgkdS$aq*#F@dLSm*B8Tv?pc38h(*~xJ?tiQo^F&--A`#Qc7hriz#Gs6-Du9E`S zJ062aO8Ommlz0jbzp=vo0mpOj?UMf?JVBgzoPd6%^(j1EGOTd?g5wQXzhwN13Mi{ke~B^nPLHM5a10#2JR1F z{V~D1c(0^4c6>VCFX?R@cfi4K_1W`(7Xn{NfeZ1s;y(D8xWD5eI3p*V39ofL3g<}r zt++(mjDKzr5YS(wywmZ$j_=3ej|Iid0$f!ZcoNqWKaIm56N;JVaQf#M?fe@v>j;Fu zF2oH2$FJdTQsJA9-^INo{Udy-_#o~p{u&SP?9Kl%^L-Gon=xj7#Dk>3?|7&<`!bGW zaVf{;9mjF_dr&b`6MH|$Jp0jK=yc=Gz@lc0Aqj?3B~((}4#G+$9Y>hNp{{VEvl(a>uLjJjwqe zULf9r7m45S?6*$`b`e-A8Qyoi-|-iYzjgdy$3Huc@R8cm@GsTIOs)+`2TsEJwdjhD z3-D&iUk7gyH*(w@Zv!{?$Lpno>(%!B|1tsnF@SAY ze+=L)yhAG3gLjEP#(Tw|JO0M;QM^B$pYb0vKM^=271+P*tBzv*F@Su>F~?OM*LK`6 zw4Hxr=Cm;2xwYf7@iFOOXUE;K{yIS~tiMjs566RNZenH-uIkyF|6}GF0yQMV4UTVd zJl^qC$1@zybv)m<*FIa|2?F}_21~L2yuq_L_;~~JuW`H)7s?83#qGua!usu=1-W-ozoiJOCt4?8~U_-Dr%1H#(ulbIYpe6u*Qo4{BJ z?8Otr2k{i~VLVNI6yGKO*>T1d`urzj0M85aCrmj44~78~$MeOt95-~_950gmZSYcY zNBp$7D^5N8WrO@uZO>wXxao^mN`XOmop?CjARdJ`iN`sfPN&YQ(pLjbyly<_7>fHnmONPCU58|Veei$DUAH^nkULbCM z##!Qwf%@PQV*r;+r6(xKL_i>X@Q8T;*N_ToId15D6>297%Z8HbPI#7)K^{h}H3z#ztd_{}rMfRiEaxR&FFj+;Afa zu7lYB<1@m2*pm#?#C;tPay;DeD97U*Pr~yv!uF;o2rQDo9LMt=FT~3v|I?0FIbQF0 zi{r$00_!uv9__|E#d{qebbQ$HQO7?!&KO**{)8z}JYdQ>j^psTAg&4Vw;5rF8#-?8 zxQ*kE#mxD?Yq0?H05+Ln1ATD`@gT>;9go80B>yz$69Iv-l3};wy*PY~hI0W9pK6Jl!;X(S{uwXH3_Fl9ge$0& z?flC`5C%M#!{I|ITn{*|g||zE4RQDo3g-g6Q_|bu@SAXP(-9x?(!KegF%Sf7FBt>) zh*Z!Q9~BS68Cf>3U3g#McoZ%n>Em$tObF)!T+OC??Nh=3$JLzx*i`?09KUyj?vQP) zgJ^~`wk$Jtg(Q`zP%25akv(U#Mg%oE=#BTcI{l3Ydp=O89%c1DuYZEo-zB|Zng!`^K-h7>>c714c~&3y z>LpgEzwuzl0jtyBc(B_*p?ZVWrxs0Lge(16HR$ zuD9brq57cJor|U~@Tk@4PuuM_aI@Y1F7p~P?WL|?mn&3fTb=&6-5v|9PJjAs&j}0F z`GxA%R!{OSufxrD`}2d&)-cm+=x%lT(`$QOP^iAXP~G3^r@i@ySiQ`vM_HZzu-c9T z$wI-jLiMad^*pQBcne%&^(L=gX7$@%z1r$+UcJHU_q;l}rBJZV>h!1Gb{nvIpEtu! zs}FefKC2IU^#QB@^6GZ4YtKa942sV3tVyHyrT-$*j8PJg&;&j}0F*@fyHtJ5DY z+ik$=^k>QTSYY)9-tt@jzuF%EJN&=EjssSwKl`=Ef5d!C{*vT`qbj-t7xay=?|^!IAC@9Ln%8B z6siwe?Vot|sMYSovmF2JK+x}G5Z0N6>T-qZY^&3sV%c%P>Q2SeD_GC!^i(~TU#M<< zvJM}As>eE5aPr6+s~+oY^~pVGb$6@lom^t|STC#dz505qTYGhXtJ`|@5UV?Q^(d=5 zQ-_yt7ce0T!lvr6X;$0UHr-S`Hp}Yt`-IhF^Q`V04cq)nthWDmQ}x)gLiK8^heX5H zzrpGWR_pRD*x~~9RF7@5db&5m4y)&T_0B@|KC7Sd#t&GXe$S|S>|mk#sMW_#UcRkB z#;yJtC#%OYtG?5wbgCYXR5K= zKp3Piu(Q>jy#;i)`ev{0Rj9t+>hxPS)nomwe!%M=V)cBl9%c2y^zZ-dHekUDuVI?i z>%4lF)myxJUZHx4)!V)CWmc!(pr{^OZFTg!AO30H4OS;JPQH|~+kgdrz0K)&a0Ij`Plb+%U@u)3O8AGErrS0BCAZhv*WAfvxMtM%$ks~dWCIjb9cb+*;% z-vL#Rh}HLc^{Au;)4X7U)ziItn$_vw0oifD>h$k=>^NZcMz4QKp?aCsZ+hda zt^U%hlN&7f&I`5_s<&DFyEneW>Qhg?5~?2CX?6P5ZuQu{LiGWwPxtx{T76c!PR5Q} za6vkVW!z?WJFm{Px~Er{vpW475jzf8J;)o+DOA_9I{ljvyA4?V@X7w99SAH)|HQ(M z16Hp%*S8A!jqI_YP(91)^ovWo4Om^x>tAAZJ+EG7byKfiZFTfg&Tjv9Ah00)+RTmvR;OQj z*<*p#>6c!194J)pv^xFL%WeZ!&-5eov3geT^_G`!^{-(aKK|Klz=FeGLx)0jXRFgMwe0zT z)!8SX<=Sz;>RMiXeWAL))eXJzAy#*y4lkeEfCZO&4HK;H?bXu?)w8Vb>y6K|x}R4s zv3jIeFS9!RJz#q*uzH-0m(t^%-3Ba}R5aE0mPG617Bdrf`*Inl#NKyx<*+Uq+v{q2 z%Gm$@DO8so$cQc69*oLm6pvMRH5vVF3f1TQ|2i4#@&AL^wT0@l3f27z)gubkvkKMo z3e`*gUv1aP=KmMOV!H~}e;2CD4r1Hv&}w(%av7y!Rb5TS7B?(ZcPLbM_G_N^b@v0S zuP#(ySE#Z}R{t(wIeb~&djs_B$?x3w=fC{{6ErDeL~hg>^bF{9_BxFDOV| zG%8W0dO=gmyY#p$zgdBy=jAzkO~xiFHErrPy1Cns@Nd|_zPG*6+-*kqHymi58Z$PLSG6ev*3cqc=gWJ9f5V2B zsaI{$RhzoaZNVDY)`ov+Xx2FO``AR~c#~8jnW$r%mY+;C$!J!v_9puq&uG>()%Lm~ z740f+mRdC~QK@{hXp@?;-@+xdN{zd&NTsUHxolg9mhK|Lzce&&k}5SmQTZ%##@R|+ zh461U&Q%=3ZGTI*nc?4Xpjqnr@rlo?hC9R-(aJ3%{2Mm3SbN!o#0MD}&0D5wPfqMA zThNNtvt1qT@$>TYQy=9QtCZ2aRjS@SiSLswSO7Dg+)*u>C@Bc%2#1(8M6`Y_++85w z{#AK5>2V<1|+a4#_s-BSuubEhP7QZ4RHl+9&s##kydMOzTvN%QU2 zeS5zm6;sWd7At)Yoo?TUe_7$^?w)V|OuDu#+_khu_nhdK(5hAHqkHYP)Ohwey(?BL zXzVV$z->zSmwnVYfA$@{E7mS(%o^K*3f!89f5QfM^%OL2HhXdJibV<&x0qHer#>f)J+Y`a!I%q@0%xU^>ZsogW} zU=$vMY(1MqD=M%+yMOepRjfj)+{{G9Mg?v!Hwh6Av(Lgg-Cep#fxGDxG;5g}biWIp$E}v(UhUFMYh`tKb#-YLE{)?_c(vGHE~cmD;hoCr@D^b6xxe8p+QxN@ zw7%4WTE!}5>L6uh{@Trp5}&nNTVrRUZqZbqJ&7}l^SnH3V5!vYbL>z*D7A1;qEeN? zrNT$BO#5#^D0}p!?T*xTZ#df<4j3MiI=&~dw8~J;-Ps$qUmnJ?hNWKJo9IzxxQ0h~ z!}l^gVr{*z6H_wcccxZO7Xi=hxRAVj}K3^{Wkn_&$o$2 zRYqw^JX?uY^8%JMI<@`VL~e47hKG5>;RW5T97nT}WZkKp|3*2B&a(5ZGWi!%)9S2o z%5l`syOvqwm8Y8SO_WKE`z~>Id_wBg?-IEgBTJTL+g-PE`+Y@I^h{>2a%$ zqx?qlY3j#OK8V~c6G!<-a<_aO<$PZ&T7Ef-XkZBg_oeC`NaV(+r!GEVdwY;BjxRt( zU7yf3BQ^Fw;`}NzHT<(Te2C#usol(dzlM1M7tPK4Zg#UP^^*+`)G+V7MZ>(SmNh8V z)`sIVQayi4oS!kXRI0}F@ru!HxVP8MTV`4Jr?#`O(OMYqazzW9%kYfUaa&5XrYpSR zbqtS6_4zqH_b1*kU-q!MXa1a?`rh6kli|HU@s{}q@Fr#^Nmj(RdxE2#B1^4Yj+<>?1{%Gr(XTn=AWD@cFbBQr|KQE`6s7(Q9YCz zd(1B4p|!z(iM$Lu3S4}`28X4_p0JA=xpw!7L^5s*>J!f_onaR}s7U7dRotDSyS*5T zR=&46Yhr49kxbU?IDf{c8lIBLnsq-VvxglppE@OTsr|XQsErL@JGN+Maa(Zeo?@93 zGu%->>Fy4^T!|KZI{Wfr?JXSTb;uu4j-xz0@Xb?>qx>xTA61T{oCk+!`Stuq3SOQ^ z7l5M~`jF2{Z7ZI6r(HiCPR%?&V@j#iwRhX$^s!SjtJoggKsK?|>^*}jq$(#eo0vIh zR!H?tWLAlG-VSe(yIJJa)U1Rpa$+hoGqX|Db*gv!D9=5AYN~r?=Cw`SHQ*l-IbmLO zIKt8TcG7J7yruT^hQp)Y)Kt9^;T6}5KNC{ZO4v0vKDE0&$old z!jjf9Gqt^>UAHq*2UX>q7FL~33&%#-pNtu$QojweowD&X+h}(n<2!iKdh!0a-T!~p zET@+W7to=UZM$pc{p_guLp4wHhB+8uC?~XdT*{?UAo-Ih%Dv zs%JUdSrbzWsivl0Qgxg^6H=YB!o53Be}2u%Y;#@oNa`)m&YE%ZVFX8)bI8kal-IO( zCbH)129Be=DfxobwDPtscH1m(t1v#5QNdPWLaJwl%-rZvoVN|5dH8Nb*22`R3h6uh zW8N@dZLmB0!3vp;q6gS@-tZ=d-4@|!soTTZw1zm!`IUY2AdjP*?;}LZ!BNil60#F^ z8@4I+SjEh`wq4sQhTB!HQn+hxu9TVE#H~S7`+zr^v%NWbc7>yyAC24gZm1OAIPxoJ zPPALhOZwBgN?5(BKOM5IWkPC0c4mhb^|;$UrtQJe(psCdYis>*l(!`h;;C!*CMqPe z7Hh^1UPBjiv_Krq*qa&G+kb4rQQp^_wM5IoQQn{2tr(8-QRI(@{S9Ii(ZD2c1{~#c z$e*w$2n&l;?Cch}&>KI%_>)=z9L@I^x$DPKp24x`Fv_htCwBDV5suCp+D(QwOi%Vsp0pRRNA> zhtt`&X~l4q&mm7K$5H+;`G7FDe_+%b;PjhS z-GXp5!xD10AROgSk!wLQ9OcW%1N)CnILcR;vzBXxa6v59d1$c``OhliPaR(oub8Y_-%^`)dX0O{(ayrr0uGp~y1N>V@?YtHPAh_= z{3y9wK^*0$@QAb`?C%_-hz9r|-3{0q$>}_N;;tFO56`3i@EQ9`U2x;nk2NxLlbvj~ zsyy`DgrnBZEc$u1;wbM)?$!}Ud0%pOkvPivBs|J-l#fx)Gh==H9S!hFc9vU59OVz2 zv(MCuKAhTIGqX;%TfL{ft~Jd4tUX(JxLC!cTgt26_}eU{p_YQ9rEfP^bvcgm9p}b$ogs z4YW6Bk5!JNyd(KsnFi@ZY9Od_ztLDd3?T#iYM&r}Gan2fPyEaw& z*F?qSgW5BE4I!F;zBzlAavbH4k>9QyNBI-xs%|+r$~l>2Rks`*~e~(F_N@97p+aFUL{NS0%EW>KegO z&Q~O=x*SJ&IllBAJyYPsjL`t!p@?R{QC`y=9pZ76^Zom1Pv9u$yB68av_d$_IYnYs z@(Oliricc@R|rePR|r+yp5?3a(J)`l$Znn*WG@-2G}mIU^oIMIvzCUhD3WgO0p9Qk zhF9wbgrlWS3TIP}qx=DKw;UYh^T^$DaFlb_!f0Xd*tp3 z9FFoY$lY>qlZ)Xs+pq;#!()Sy9>rqo^Q@>sSCzY-qKvv%K0jM)(h@7;pO%!C>C#}6)K5a%VlvJnHzO7ewK#2?Vs;38b{X8*@`gt-)Pk8_1C#~`8Gy_li;%tUZkKh~RrCtwj2giB0 z$)-2(O)^i>;y25C@GbJsxSxCk-zpa`VwWE+h_{5}{WVY<-zMkd0dhM$Q0{^U$yZ_a zw@tTTx15o9u=2_H4tW+Hf^GX3Q4G}p-ztm`lh@sP%z6-CF@4@RrCu4Ic+}d$gu=sj4J%>3A z+QiAY;xEf@;f?Yq_!aqU{Hpvbeoa1x-7-1DRD6^23V5@-3*}I3(Lf{oy37fh;#=hl z@f&ha{HA;ZeoG#V-I5a;u8%l!k@}d;?Lw~@#pe-{Du4$&I&gs_8$J9 z@}0Q6@;!K`@?S8|JZw6c;P}5w1F=jSsHTCU_)FzkxVrMn_$%cBuBf~|-mSb9uB5yz z-lMz==JR%&y5YU#w*7r5zE;x!%u^wo?!fz$kHg={_v8KYLj0}#EdEY@8GkQt!#~Jh z;2-7haPokPzbSr_iBFc=?HJ{IRHpNRid{vfWUd@lY=`4jjmepP@1*Fi zfj#&i&G0=wEdPm*$XWK3Hhxs@fd7>*#>eDd_&<3tJ}ytkC*;*7?fQ?$>?w&&pED4b z_h9$KnnSpV@`|UKc@NO0GjUP59p?F%P2F&D`38KdJRB$FDLB*2{cko!2@R~rCFM8p zY4S(7l>8MgE&qng$p7N9ay5HN5a&%vo6g2rav#i-MVt5(JsGc{VmbpAWjcix%D-G^Xkb7`}{iIK(2t#l&j!|^69vd zTo3a;uT2ec6S)BA%k6N1d~TAWsfr75Gx;*yT)q;wkgvfl{925Qp!`#Lj{m$CwkcuHO5#m4Lpj`7u8eP2 zPhIS;*L>{W&~Ag>yWt&i^o}_mtaPQG zJ{aFGPsR_(bCVP;HN!LbK@GfuXUU)A+49$Tj{GBjNdCo~J2&k-mmj7lr98u)*v220 zGw~zt_+QG3+<6+nc{adQg{DX4+IYTP7cY<-;)QZ+yhv_~ACu3;i{(r361hhej{lFV zxRQY<39S-;JM^C*Wt~nRuBz3on;b_*waJ{2aFX-%^Sd8h943l-J?s z<&Aiiycw^S-^VY=AK^9fXZS^VAAU*x4zHDe!pU_iezPKXecE|0KTOjG<)>tuUzShB z8|BmRD{@8rs$2uVCfCJh$HTKw<>O7tlWi$BtLT8Y$QR+)bD}Nh* zAa8d)e2ljTf2jN${E_^#oz28v9;^7Bfsf@Q_!IdAwhLoHC99f0RbB>vCRf3q%hmB0 za)AFQH^4jP#^&5zX11Rq&keMq=}R@8hrg09#=GS!@E*A@-YegVzm|vKee!7hjl2Nc z#?-V!&Qs=Oe7^=(Gw`ka68=tp8GkRog@2IW#Xrg);sf$m_$T=Q{#ibRf02*kUqdHj zCn$c?KnZ*OAOBr0jsK9#*W%%>YR?ZwGJ#kUl;pi}KCIE3?*AH{X#A~no) z<>I)WTo%`tD`Hy?2fIAnKzV(9rrZ=al#?wf8mTx3HWF`wD7{kti8Y2bT&wfqaV1$|-VA>3Pe#u?^oq&}1D&E7l z%YWd(a#oJ{4!J5GBG)kI4oy4H<@M-sPurX0Vd`m(hs*8oV)y+2GKvuzxC-AXUt`W4 znRcGb2hnqv@*#MXJQ|OdlX#4LFTPuzfyc@Z=GgV0>#hPTVtH<0Ax-1dv;>crm*WZY zN<2|shbPIe;K}l4bM8H9=ec|vJyXc-{`VQhy=vNnr^-LzY4RWVKDk(~dAeL0&yXwQ znerL6)eyd2wweP|c95id}_2``l2!HeYg@niC5c(J?- zyXE|Vm$>8q&lHcV>2Lgmd;~u!XV?b~@u%eCc&S{%ocnazc`h$c&ojzv;bn4Nyj(uB zmRR>*ND?z5EZ}AQ!D|ep$}K8|7;F6}gr<8Gltp9V>EQOFPfy z11**8Jitjb>BK|?%fPa+V#0TVU_$T=T{Ik3Z|03_fzsmdZZ}RW>cloctZvXK= zR2*U8pj96G{5pO~%YO&=P`*1!ahZzm z@#S(>eREH_8ookqh1Y69?eLY#FT+>KLvb&848B@^1h3P)i*RrEueoof;9G|_y^s6I zpW$oeJ-Dx2tbuvG7E}&jr@Rf`p!|G%z48J0W#uFB4ay&B!0~^h23}&|Mh$$0Z<2q+ zH_LxxcNmVJX}(2y72Hp*jc=8k;QsR2_%``MJOJDM?{bQP8n_M*k_X}2$WJQ3d^ zuf#*-xA0K;V?0dWgNMsM;SusbIC-avA`Pt=DW8S!lF!4VXh3}VN zY{c>Z0Tr(^@Syw_o+ZDBXUm`CIr1+2kh~Yqm4Cu1`8WKqd;&ir7inysXXf~yMe(Qx zD&hHZHSAv3<>3X&>*IxTYrIHqiyxD_;>B_|yhQGcAD3@4C*x13xSfF~5q0z()MM{1#p%zl&GPAL19}U3iVW z2frxq$1lmh;kELgc%6IWE*KyW)*iGen);2Z*ei%67W6 zaQ9wmWqbUa@{ag-xhwud?uHM_z3`uMAN-e`yoKU#6}RC-@-Y05JQ5$4$KxaNJ@}|R z&7Avh+IcRYN6#_k3-N#QQheMU|Cdvo(7+lTD;gfS*WtLl8M`z3zJZG<-;Pg_ci^J( z=eU@>7Z;ZgMUdz?QCFS}!p`OOrZsCJ%UEAYKKbH4=!!y_o z71-y$6uBCx)XW@R1vt72usd6GV{BIe&m-Dneqmx$N6c^XZ0d^h z6}M2-mv6%jrcf%d!%bRojKTky;20F>t2`df zJQ8=7$KZ?P$+(L=4R@7insYBsJ1@*_`{&Vgi3S$pOXa1wo4g!%m)GDP@;c1#F>Tt6 zFPGoIJ>~8A3V8>*QA~2^3&<* zqdW&+E7!+;?eZ)G~*9x4A|&b=$`JeMD)XO!||c(h!!rFo2;z<0~#@K`h3Uy;Hc zh-%<*YRbjqta+563g)yYNhT0={3q2R|U+haZ&Z;8~%Qv1cjVt$iJyt)^G-9C@=j_o1}& zT>cR~bCrLJQ}S;7u)GgHBL9Tv$-m)8-EKcjF<%46@B+DLEAv7*ffvc;@MCf%yjZSk z&Rvprp3C#-d0csY{Dj;zN%5o#JMpx=?2^yHOXc(M)AGgm8M!-NCSQ%0%h%#(8hEQ*2fraVz;DXU@LO^#{I=W{zayWIx5<66yRi?z$#*qy2gQ5xaC7eaY3I3o zGCkXsPsJa|v+#%VT>O!|2=9;|#~;hj;!ot{^Aw+|SdTxGU%{WtZ{jcHck%z^56rnc z)6R4GE_!w;--ExDf52bKKjYo*`2RP>9t|A9d*zHa=C9>qc%NJveget$YUl zPHv6eZtsM@SKg%!$A5b=$-5@K8Tdg1eesX-t@waE5dS2Pz(32Q@Go)_|0>VGzsa-l z?{dnV`-hqPA4Q%USW44DH7&<~%4_gn@;dytycr*o-@yOK+wfueTYN&_lovV6{GVJBAD7GE6LJ+CD`uM#i&e*QIR|IR7vUmuUwlgFWNbJ^Q4LJP z?%x`mjf*RvkKI2x_#{p!e+dr@FE{oEJ|TaLGu3kl4_99FY_t6zQywmnR8vce5o$Ug z$JNvymsHPae5ZP*;SBZ6#iyxf5iTXK#--)$*sbU%xQz1OlN4_44&kyIDA(5PR;V&A zr@SF{E725ZDL)&RmpkDK@+G*Ud?l_VUx)1rYWd14D7PJO)>lAHw|P-KJ%@ zy8IeGUH%T&kWXu8t|?c&atqeR(o&AkV~S;$$rLI7LGZyo4LcA7l4Q=u7Ng3GKs;)pOMADbhaOQx7*$PXTuS z-enhDp!`albYBR$k%IGE+H^N=CO?Cl%Li}^`4Da?XPjehC6~gj?JZ6=t^o6N)P}@P#?|s}zyw{|i=klWGT2CM4W$?9f6>~D)S49p3*U63X^>R~l?hR?@ zx%^yuZd86gzDd3myMJu;GJK2jYj8jLI(%#BWUN0$e+>-6x5;V(~Ql30a;l2`m3_qrUB5`xgB9_Er#qnb0rSKBD9DZD`f}fD9<0s`>_$iqm zR>$XMgv;!Km%8Ks6%>7ngxA>(o^Qc!O^0K*qIcn^wa_W}8TmfEOnwBr4OoDeD_@S? z2CU@w$I%wN!hqX?&G=c(z!_HJ&&eO-74p}3rOYWn;?K)B*#^c}$+zOw@-WXM@e7u7 z|KrE0VPT$z*JuWQVHw%Js%uGJZ+<$DTjOYnAgOs>u96D!xv6S-WDJJl~IBR?fF_os%)XWgFk90lw}TnXhujUs29? z4I}eC!svQE&F=V-%i>qnpXa$geogtgp3ldd+@bfnuu#)Yc(Xj-^JMI9IDCR0c`0`H zf>%6m##^);K6i}FM~czyEPUH9DvH{xsrc)fp{nN^*xdkIcy5E;jj@~O%dxxr4fK2m zc6UYI(Zk{KpSSCxTQcuCMdtma_*ShD@0LX7jga^o%6T~-nOEx3{hKFJ@i*1OlcD%q z^39(6bNTVNHNYcfWF7>g8zYD5$Q*T}8za|5WHu@~9&j6u%*`h{HoWKgLmb|CZTr7D zS;S)BV|SxFM&ktdDV_fKY33gkv-1AEOzV_le>^5v8b{qBvb{qDNIT`Kh z@YGt{HFLF0Yx+RW^_+*@7Bu$U6uS*L+w(ctT{RbY4o}6Ut0@_~oB?;8U4`9s*4Oim z*zJjdp2Jggxjiw8+<7eKPHxj=&r`A6E3-no{6|hp@ZXv?*cUp_OQd-BsDe z^QG7h$>H(8FNM4QZp7|7y92wcW;k|N%>?Xr^*z|_>e-%C_#LgpQqRk=J2Jjpg5$rt zK3-$MT?OIE!JI!N|5yv!?Rg(|x9Wq~T^)z8+wDb4rd=GnBVZ*QULCgmRVmz6PzSr+ z-T=E@-OBUX*xkQx!){mKjz7_gj=*k@j>2w_j>m3~PR7YkHA8q-GUo>vaJxE%YsSLq z(a`qv$EHQt?e?dz+l$NaXIkc}us>{%y@=fna6SH9J;_a8u@!%zfp>9D&Abc$PkDG| zw4L&w~2u+^o$znUFY(+1+Xe6*z(~(yuv`8f{EIvOe?#%R7IeV# zulNt;;rZX(3Weu?b9vJ;)^kujE%Bdnc%C=sbIIMF=;QhN@ceIXO{aPbnt|P#K87pm ze*c8$b)GkR4o}18uF5aTosZ(bv~tJs-|qNdtgIF82$+f84Wu0AxwTCdu{$!Jj<+h$ z!S22uo~q4Vz!l_n0Zi9=z5(;p+NS>D`QKbIhyi<0;W`_R-3r}>|IrNNJx|7NMWVMeteC#&haU5=d-T#(S9MKFbJ->k6mTkrEdEDEccY5CK`7h6hJ!h6nFQ*hv zx`m!W!K)0LYU6+9HeTM&%P;rxtGxVnFCXgVQ}Hn^a|ZrTPCo7pEcFIn@$$`H{<)X$ z^723MaV_+3e5r1*CG35-C@+m&URycGKYI(#8))avaITkMg-_@LuE8;T#bMJ>FTc~v zXL$L8UcS`Jm)pN@e{w;a8Hj74Z{Q61JF2m&xDK&zaS`?Zh-Yf+|Ml|l zjCU^2EpI*H`Il|}Jc=orp;a_s`Pp869WJU1xCs}N@5069yYZ>=J)WmwUbWh^!0TUZ zPR27e@C*aa&*2ivH+eH`#U+)$=lMg-OJSS7#%|@l#ct*P@$!Guc`|ldh4c(%v6~^+ z%k#XvH7=zKYl}Bt5!<(VEH$z`< zhH)onh{YyhPLXcYeV*^fZbcu)Zbj$g8tQ-C^HQATT_c-TP&lu~Zp+@pXQ*cz&XGUD zx$>u8|Bsk=pKSUCyA?Wu-SYqQ`jZta+dys2kc|WR4D4p8jok`0$9d{$jqAu4dHE&S zUGS~Au6hRIdh#$?$N!NOZFCC{&xq(ekv!U^n6uH_G#@vRANRZz+q@i}S2)wZ8oTAZ zh23rI-O3#Q-R|ANfV<8pNq&d%I(I74*dJDhSVr==ukcx7QX=!)I!*gl9o)D9r4+*mWeiwktYJ3N1eo2ch2&tKyf>OY9{!=7a95QV#d;@Kw$VkNN48{h&h zv@vcfpY8b^oT29jm*QsXxePa#uk(BpZsCsq!@@#MBk>qr9S`Euw9toeOZBY7t>lfk zwfws0w{RQf;mHV{caWc@{OhDwe2dHK!hZG~o|y1#&2VZ}>uD>8CnIbp=aRRV^YA%x zL(loRgYpYqe|Z1vO5t|bRoLyWYcOwL+jKWRS00Z$%J+Jnj`@cnY?_VTa#GkWCp^z! zC-vBQ2JQHNzKU09x?`YVx z2D{z34);+00lrNB*z-=$yYc1fDN)_rQ!b6KkZa;A9apPjQ`^X5;JSRrm(^Ma;(`HofKfU3`=B zpRwDR-|@}L|MC1Ub{mj!x?O&>1;tJ`yDg}O`)cNz_!c?ObA9XXpY@-F2Zg( zm*9R{POqq+?e9Zzs|N1$W*F_wFeRG7`tS4lpY-~l@%mr%`qz8?pL;#K%*l9vUGR5a z(*bNhv*cm)Z_h{YZR!tCI_S2hLJiB^o;U-$jj4^@o@k3*e}{B`GIp^yLw9e60qLgL zU|ifjU9@Q=9-x&NgWX+sme)Vm>tBHX3uo221>L+)h4Z@O|8ri`o7ip5Hat)l_KD{& zuv?)$*e&!MJV^b&c>V*s6)jfN?3SOIq;Lz$!frv8u$!TwH$%SH-^S~2=k<5Tw`(PO zdj0*pe1OZ7F`LGD1Cy}Z;+dXjVYi}>d;Lqje1n(2>g8`>cNKi$@HvgcFPG*@#mHko*&Tl&!Z>lW(ZFT=w?{TfSX}Cb~C(+hiWCZ zVAubNmw(~qKYRJ_INah`?5OADac_pwIq3@sPdex>An@|=7LkaSa=S>9QXbIA>IskVYkq+-pmucnWuU^Phxk$&*0&@uopb9#cqXO#cqYR z@MH{b_@Cwy9NE@^_R{~_h(_(Uk$tdnwaI=<;R*)+^Lml z<;~F5%e#5`tzJG5yDh%c^JuSsrq@5q%a@w%{QtV(<=((*?5=`$z5D|_QvLt){1xt1 zG<>|+@A(I3+s4>&?Dj-PE%k@@zls!YAUiz}YvknxUVgrpU*zT8@o_FH7Q4agxy9=l z?d8cZPwH0ss5h_(yRCf2^K;=0+R8Pa*Ew^++p*i29bW#um;dDD2g9{+Yh9wY&1}1o zsWiSzySj?!>Ue}!qL$}6&dl2uyXAE7@}6Ga%jMkuVz+w(L%kWMc=>%^J`20s$Kzhl zQm^M_FMrL;cX|1q+8qDgR{qF<^Dk}&o`C%2`LHu9T`EX#WjSnng(=U=>w9@Vb}M~8 z9>w0Z?Z1d(w08C7p0C2Qw9o-3XNbiHd-)_Uzt_tj_3}kt{(_gU#YuMouTr?%!3SQ` z$Jq6J@8v&vd67Kp3AZK|D~{d9l=WNzyXDmJ`jZX3fp*@&xnABGM|UkRzXgve9`5!# zJP*e!bR!+(d7Lvd%*C_Z@qeB-utEdjLRWkFTVDRIm+$fNZ@l~fb}Rj_*K@+_$qLW^ z=5|YXraD_|rh+=*+J$a`-BzCCIXnrS+sZEF&X+nf^Uc^TzrU9!qn!KSL~mdkc5D5( zmoLS47Y|oxmFE|+{kV{ON_c`f=kNq`W~R@)y!>@9--eHe$3OcHAjMei-tVz{UGRtJzp?$Gkd-**IaW_O7giCw9m6yMWZ6m|&f6WzaL3pM*x0T!6 z%xv*?XL|Nyx6(g&`M+L%!pn=*Pp@=UJWhMMMtzR|?k$#p0r!fh9(FIM&+%qB54-+u zUjOA@{~cccaIb&7*FX7Wzis~likxC>SZpzNKbu_RtLR^H9?<@jQ)Tz~AApBzMS zk7gL*c@%aF8t-{Bb_+`3URvm*c#4+uBEDB%kEhC;Ja5I*-1|T8stB)w5AY3I=r7nU z=nrgP66VHu*z+-bpDv)tnQ0ftZaLvO0o`)ylDp+JKGUxMsA$T-bSV z1>NK=Xn^Ozc!riU((@QRRvR`cGWWlGDQ0Sh2RzTgZVQ%Scfl*L+kzK8ugCXmIh#Gd zf!%VxasBM-@6E~h1DfGi2AmJ#2bKSe-CCc(vy@kCXttGL%EoTqT+eyfEvGSd%V~-`d)v&=jP7L+r{&x*p`!IL07l}Uya?Ga_z?F>H_ThLiSd@u5bJJ{^U`2qYY3I580(#u(`Mc0F zOAE!(Y5Omw=b;SyZ!C71{V)3G*l_gEv0Z8ApIu|>ZU4(ny6@~#ME~qsUvvBst;CHq z%}YDa<^AXxeTw}z78_vyi_Qg#qjP}{HplDf!iLdPKkeurIOX_bKPuA&u( zE{PY);dQ)7u15ZtToW&rYvCnwcvU_wHz0pP4zIo^-SMC6EdG=R+T*2iNBp!LUIov{ zUC5Wo;cj0pv#aCJ%HghlPG;}MSIFU>TPY9V`A__L72)n$C9}KYtL2gS1$hi!Bag!` z%9HR*@&kCSJO{6nAI9tD`FMkw{r?!n%NlqBZaY> z{t&+|e}cElUzn5eH&lGdz?<@3{Fb~Qzb*fc-;w{q+vLOeUHKS(Pv##pioY-O+W4^Z>3Q&%rh2hw&Nme9Vu%YzIf@(;L) z{4>s%f5!##UzlI6G1=3Bcr*DF++02tw~$L>e%)qMW!%cl_E)25tpWQ2w0&tru7%H% z8{@O(rns%#61S7j!tLeu_#C+-?jT=)&y~BFlktu!_*!oKJoz%*N$!i!mv6)u$o=qz z@&Mdf9*HlK$KWpVINVj9gf9+l|NaZbB^sE6FO?t0-Q@YWyZkinAwP>Rlb^?z%WH5? zc@w@u-ioi3-^N!ZRlG;hOa201Eq{r7%X{%P@_yV${vBT{|AqU?hw*jtF?_uoJH>p1 zd`gnyMipi7O>%jBvs@Y9B3Hxxh_oC%P+*LiFVE4|*7udZB`4YPqCwsAbL9!nw-7AtGDBO#UpRs!p@;i1fJpRJ& zg~ws+UThr0bM0uvWbfuiFECEQ?)Al~VXjBZl3^|<%TTx%66G=1gH4sOd+|^WyB7~9 zIOYPbw2QDwot=;X>DtNMb&Y=IEw1dg+_@BqX6b<0OH8mu^S2+$`PgC-# z%5mU&T9Z#xjsw@z$sApk;eqZx^<2%rKbA8+Dr^Qfa2*Spp&SRUhofH3OyxLmJ%h;a zSB?YM!_hG30p&PwJrl_vw4CEVMc|sIF)&Lr;D9EZxC!LUPCIZt50lSPjsw@Ti2Nbt zIB-2rlFwC+lYwjEP@0p{01jN!YVwDb`3U*rX$P(+W_OsJCzRvB^%N(6QaKJ>4=1h2c}n?d9RCB?l*PbO4dB2v zRUv;`ISyP;P4Z`yD)|!E5HGl)x)RFu-?5K^&BC8M>!5$508X7+mz$L_3(wOoOhKM=kw>lHI-uEJq_T%HB}&gUpWq3 z508pD+m++M_2iO&pd1ITrylu-%5gvs_rE3#e53&!xTcomJCx(V^|T}ZSUC<{PbczE zl;gnlbS3{(ISyRUW#(kgXBwagTod0N$@yG44qQ)P@-LL*!1dfr{y*h7a6N;}(KDry zc&BI{0HSYa6O!PH|Iy?IB-3C$qy*Uf$RB>{3mz(rwCjVC$Y`>Su^0k znwb6~|0V6f^&BPtRXGk^Pt2Ya=lrG|2d<|W`R~fj$ynf;N;B|>25{h-Dv}>mjsw?I zjr>pLIB-2V> znmFM>PD$lBa6NyJpQaoKuIC@}Qp$1Qdj2CXtsDogr-;4!$|++x+fNa=rc4IPY6cw8 zWK$XPa%l&yrxJOVavZpx>g45>2T>p6!!TR9HYlZ>6uKvfOkz%_LxucjOauBQh%=V7u52d<|V`RU4W;ClL! z*HDfF*K;#@&7=k>0@pN<0Z#5@6Ao(1!^m^e4qVS@@?7OOa6RM6YbnQp>zP7cTlo|| ze+XRD35H`hP|B|;*jsw?I z#E!1fdpoD$mg=c&&S{l);PTVSTPw#w(lym)fU}6%gag-9pZqN4IB-2p$j?@e1J~1n zysdH^xSqD;oSDof9Jrq3c?`7I01jMJ7xHtIdzVGk?KY4|PePvh4gN;HyTD-W;|U5ww_9#)zw@!QzLN^>24TYFe(Zo&7mhegBp z-<=HGd4QGZ0sQv%u+ogj?_dur%>?|8_OQ~ti0^F=E6wYL>#C0j4odVkH}tiKmF5Hd zPWG_Ue2U-M9#)!f@cryzrTH1(ztz_b4obvNT&e-S0hXi23`JD~?O~bEvWJz1FPUmDdsu09 z;Nw?&d*FvliTGct+Q%MN8a}CNUwc?-xUKq+J*+hQ;RoBpN^>~Ah}PGC4oWnfVMzHu z<$}dsA)hQleCF+Ym)gD%wf%wzxVWrsz z&sS=>V5Qj~e}+A*G(%@CKmOI39)>a`I-VQOvWJ!CEd1H_u+p4|KgS+cnk(=l>|v$3 z0e^1seE)G!qOlAkeFH3_a@~bLugOaDApU%NSZSWd^Zi&ZSZR3k$?8ITxK^UK7%uVv zE75!Si|t{h`550~4=c^L_)F|zrTH0usXZ(=bz?dMuMp*em1vd@#b0g@E6r^9E9_yV znHztlJ*+ef;;*uYm1a@=sCVWl|@Kh_>rnse~Hrk4v=nhSaV&uW~9awxxHV7`nSZf~;EjK<$# z4=c?W{GIl&(u~93We+ROz4*KF<@4vDM2|4s;~QYLMvcZ(_SYU&ZgCa9$+P!ihsx+R+_Kz58J~^^CSKddsu0v;UBe!m8PD~Fx~^K zMD5ET`m7$ahn1!i{&9O)Y39N|VGk?K0{AEGVWp|?Pv!NWgA(x;tku)L0oGII9{34O zR+?4u&)CCCvo`)&dst~U#6M@hVaM|0uS8oiJnsQkqV4f7*uzS*6aGbeSZQ{_zhn<9 z%^>{C_ONL9{u|8jiU(MUcthFhReM-zj=;ZW4=c?v_=)zg((tCU)$8`K(wtVfuHNv# zL5W6i!<+W7(p-Xn%N|ymYw(loVWk;^pKK2+&29L%TYcT&phR~wyyF{SIchwJf7c#X z8s5CNde0tKnhE&#?O~;P2|vXi*86$=f1Tk253mxwjsMUdRvO*~xBAE)R+>-oAKSx9 z^ELhxdsu0HtQn?y_>m#eH2kObu+mJ&e`XIW&5WIj|J)u{npyE**uzTG1^;DU|2ZfT z@7G&> zx8JL9^+S^tzaRcbdst}>#{XmwE6vgPpY36_Hs$;86oy|sz)Ex;{#ScgXJ*+en@PFCEO7kNAZ+lp2CgT6Ghm~eBe!6|l zL5Zd?{ObW$qN(^s`MFuSV5RvQ-^LzRnqTp4?O~<)3qONBthW68yIk!%mw{i>gq5fx zzMVa+G;`qF+rvsTFMcL_SZTW9XSRoxMxD#YuV(QehD6J91HYCk7pycZ<2%^HO0y2W zqdlxN8{s?I!%DL)zH{+>{v4F3FT-rU0TxlY2IBceTv%xa;b*spmF58a9QLr%9EP9M z9Ja@bz?Ncd>&vWx&=SK zJ*+f$;1{rmmF9l@g7&b|jK?oz4=c^{_-?%aez`a((Hjg4+r#3Q!sYq^zetmn=JVqJ z`^WNrguA=pov^E_$%=0~Tk$+rp57*oN<@4qD&mhu2A(Q^2u?O~-^8NZA@tTb!lm$iqLWcwI*?al?8|R{ZPu_3dG$c?Z9NJ*+ez;y1L1 zmF6@2MtS|`phVv=Z0s9gZDjr#ze$so=1=^l_OQ}4x)i^eJ*+e{;WxLRsZ06sSE9}g zTX=w#i1&T3wzP+pWrCAccjXf+HzWYDW(ol&Cj1^tOkUW*2-Pdsu1q!}qm^mF95#PWG_U z9F5<()z=LUN_0F!Ki>e$QR7s6e|uPIM&bw9!%A}{exN<9G&kdSv4=IV_Tg@ZU7M^# z58-#Shn41W{OkI=p+1I_OQ}?h2PsAR+=C1 z``E)uGY!A5J*+g1@}~6FfAadzL5X<(|7x&rfK?){x$yfnS!ou=53z@pM)>{hVWn9C ze}H}A^56dx^<+5E1FS?F;19Bgm1cAN!S=Ay^uixv4=YU{{Gs-+TAT9yH;~~l53myL zi9g&PR+_>1BkWj9Zu+m(JKgJ$b znyc`~+QUk76aF~+nu8ME&M@2qtVH+W^PK`K&7=6^-NQ=5(-Nu^>|v#O0e_-BtP}G3 zKat@i53mwV#-D5tE6oS^Q|w`-`3!%mJ*+g};!m@OmFAb(%a4C`x(A-wphSOi!x{Fl z(lpAG4XQKkVWnx0Kg%9gnvVFh?O~;v1Ak8Oy#8}gqInrc_y$-+W=;J0_HeC48#7$s0al_d@fX^|O0zxwB70bAcE(?94=c^C_!fIuZtBL~ z43~I-m1qe5QhQiw4#8h$4=c?u{N?tr(hSF6VGk?K$@nYt70W@1&SJRAH^92md_I0u zla=OD{MGib(u~57wuhDG2K+VlH`EM?#xh*%0al{B@z>eIO7jr@dV5%Dp2go_4=c^< z_#5qEm4>gs#ybpSJitoy5&kB7SZTh%-)s*n&3E`)>|v$(1%InOtTcblQ9gb()vR+_%}2kl{{*&Y9o zJ*+fC@DJO=N>d-g@Q4RkiH^iSY7Z;TaQt|CSZPkiKV}ar&6)Ve?O~-EiGL!m{~VO) zVumMu1FR>^SKyy&veI0Gf7%{anlbnZ_OR02hJVKXHlF{YM0Ybh>j74x2l3C@!%FiQ z{&{;?X(r%bu!og~=g3qq+QXvZ`)?w{OCDe)nv8$h9#)zu_*d*)qOiehlw7S&4STPqBxUhDY#J zAK1f6GX(#kJ*+f`;y<#7mFB3L;bRXx%SVZh$A4lEE6r*6srInaoP+<=9#)zQ@SoYk zN^>dx^Su6ZP@=0DzVHpOO2l;|{>vsS%~+Yg4}ezGwK!1FS^9;eWP=mF6G(FZQs~ z%vc`TQ~hcWD@_OdZ}za#@UWw*p5}pr63xr-yFIKl3*-N=hm}V7KkZ?qSq}e~J*+gV z;QzL-IVe$2hJQT3O0*$j74x1MoB0!%8y@KchXYG$-KO*~3b68os?ftTZFKmXBY} zDCL5c<|h2C_OR02j_+U(E6u(5j>Yr&b5Nr344r%fETVEfjqluKrQyL+6@Lt-T(Hu- zhVNnzE6pVQ?DlZ2MDH`q;Q>~nsrWhVVWs&FKbJkMG(5zr>S_-w&2;?S_ORU4jTy^x zpQ?F0z)I8^Kd(KkH1pu+vxk*tA^iOIu+l7wU%(z#n&t5e^5<{L#X*TyVOYo>7Jq7} zTs`sKnyfUN;1{-sm8KVd5qnr^`r*6V_p2Ea4PvM~z)Cb2&!12#7pyb~;TN-qmF5Wi z;`Xr89FLbhtkSf8|DDFLga=rOc;;8Nq&=)O7vh()hm~d&erbDHX>P(VV-G7$c}7_I z`Tw#Wcwm?kjpv5t>|v#ufM4DoR+`uFJ?vqnc@MvWJ*+gJ;8(<#&!2-5eZ{bnZ-CVr zH5xzQSGI?hW*UAKdsu1y!LMo$E6oh$(OcDO_OR+ivoNgg0al_e_%-ZdrI{PQrai1Q z3*pzYhm~egJP(F07pye(QVeT*fR(5RejR&QX?UbswXQv^G(GX_*~3b+0e*dZSZOxH zZ;;o24ob8Q!-l>A)&^!C(^hTNWTokc-`E~jn%(f5*uzS*H-1z5z2_-E{z^22VKWb~ z5*>oy+#Xh%VfZcVVWl|^zok8_G$-M=vWG>(_um-|TYG?&Xe53cdst~M#&2s6D-92t zt9sePN^=cjNjQFR+O4AX)yFIKlU3uDFwTFkU42c%N?`aP!O@$w14=c?w z_`U35rCAZbw>_*htK;{{>pur2T8Cj@-vFybTpQv4(`2RD0zcRuRvMlGSnX#ID@`B# z5c|UA=l_WYFzoLER-!%d2iU_(voHQYdsu0B$Y6DlJ*+f`;SaWl)!LNrzhfB=@c=8) zDfmO}VWl|>f0#Y2G#BCzw}+MHO8gP_u+rRwuZMczphV*shS|eP^8o%xdsu14=W>{v4Eu2ZUC4 z_y$<5QKNAn{!V*XX?SjEb(cM?G{@lYwuhDGMEpJWuh(vc*p~+MC0%e+rvt8Fa8mGSZR0)YW1i+tTa#H$J@h7 z^CJGSy#8}gqSqN7_YJTfGrxm>qRC405&lVgSZTh-KV=Uq&9C^U?SGxW{P-);UknpG zz)IAtVAp0U$lpnW?lSC z_OQ}yjDOi4R+_Ewuh_#%vwh*Zdes95CEAG_UbBalhUdXn6YXK88H9h`9#)#c_&4lf zr8yY?W~;9o9F%An!&|-qmZQdS{3Lr=Y0kh;wuhDGT>RViu+m(Lf5#ry+qDm)7~XBN z65W7*&mLBqJMiz@!%FiIeu_P;G*93^u!oiAxtifa56>|qdL93fJ*+hE;6Jv9mF7eI zC-$(?e1@ND4=c^L_)qit&q0ZPVff59z$y{fU--|PtTb&FEdC37SZQX$e`yaZ4UgWf zzOpY|e*T|`NAFf&dw`Xw8~z)6SZNl+e`^mb%?kMM>|v!@4gbA8tk$M{|E;iucfN;4M!uRW~kdHuhO zp;7)iOSxbrdH~Hj~L!v7g7V!Wp(HMMpdsu01 z!&mmO(%g+-)E-uvC-95e!zvA5e~o7u7WV)v(JOe_!%FieehGV6Y2L#xX%8#SC-|l8 zVWs(Uq4M#or9G5G`3(c}_uQ~dla=OI{Id42((p|0YB_sYY1(uvetCOXX=cLrz?aXT zgA#ROSiv{IYK_XDe#Nh74=c?A_?7HorK#{M+rvt;6n+(ZSaqTv46Ay8m57IlSF72> zO0y1rb$eK8HpQ=D4=W81AFtN5hn1%8!?2bIScwMUd)mWFvj?8PPg^cnY4*jhV-G9M z0r++8VWl}7zaD@8wp<*PXgI_Az5&*H=F{;TG+Ajz;5W2~mF7bHM)t7MjK*(lKbq&i zE76S%n|OegXe@qHdsu1i$8Tm2E6sTP=Jv4CJd5AL9u^Is|0@hzdVrPaP5f5&u+qGT z-`XBlnoscC*uzT0`v&GPu2?O~-^9pBF$*3NnT zUx%T;2Uv+V!Vj>Am4=7bR|D-~rP&F;i#@C~yWn@Vhm~ef&9Iw?K@5oo<9D}*mF6J) z9`>-(9D(1{9#)!T@q_GPr8yP9S6=@)DA7oUy?q0$5^-IO->1n+a|M21dsu01!2ib{ zR+@46!S;pA&;JwM!?2$RScx9O53z@pW&(bHdst~+!XID{E6p4D1MOk8Hg%)%F2g|{ zU?ut(f3Q8QG+*Kmv4@rB2mGP-u+mJ!A7&3L&2)TyxCahO)NYY79AOVDO-KAtdsu0@ z;)mJ8O0xj|NPAdmy5o)*e=xweZK;!%DM0ez-lX z<2u$2t}V-<%C{n{MBCwy_YJVp^v9oI4=c^S_!I46r8xk9l0B?6hb>Y*es!{k!x$0` z=Y~`4VWl|*f2uvKG$Zh**~3b65&m?0SZS`npHV!YKL;hczVLsa*)R@1(>FiR>dQM1 zmg`CQEc+Jy zrTGnifjulYb>nY_3q8O}G(-2|FS3V~W>);g_OQ~-j&HGtm1bW2CHAn=ER4T2Ur-#B zNDPN((v`yIFR8w53mv)fxq4!R+?k*H`v2Ua{~TGdst~s$B(gx zm1acu^6{&iJd{KE4FmIq+;DS~mF5ckE%va|T!X*W9#)z$__6k|(%go>4PQQg4oY+n z!#Ljnt2HXmh{4}(4=c@6_&e-jrFkBIr#-ARuj22rhgBz<#BjFBfCpHK{=q+J4=c@#Rq+qm!%8zN{$YDqX}aJav4@pr zZv3Np{pX-W3o(rM4X_?HFN%Mx$x5>n{&9O)X?ozFu!ogqRs56ot5)U5Uy0Ucc*+B; zL>uCtwuhBwbNmE*SZTJ!KVuIoO>g|O_ONL9{_D^1oCjEmcE>+&4=c^S_!sP9r8yA) zqCKoMhvQ$ehn42&!gcks2M$VfJU6^z4=c^7_*d;=r8yh_nmw#E=i?{Z!%A}r{`FR0 zH#jKKD26wD11v|4>+x^e!%A~2{w;f0Y3{^NvWJ!C0sLfpSd(fW9%FdB$x1W<|BgMZ zG%w=cwTG2vBK|#lSZOBX-?xXAW=hR4#lsYaM4#e6u!oiA8~lg%u+sd5|HvLzn&0ss z+rvuJShV<0^7_v~iP|$v^$oB}#MK%9X_J*^F8pWqu+q$r|J)u{n(p{7>!x(xfW)CaPx%g@Ju+m(F z|J}akphTB5{NVvsqS5$2?O~<43ICTptTf~Bf7`=Kb1(iMdsu(x_5Tru=^kJudJ_My zJ*+g(;~V82gvtdg&8zq}_OQ}S!nd`DmF9y*%a4CGgNF|o5`D@IypLA7V5Rv6-_9OZ znxFCQ?O~<)13!~JtTc_qil4c7UjI2LQG142d;=_^a&^M<23}#M>5A`Q4=c?A_>T6l z(p30P_HeC4OEGlz04q@s{A~8H(yWH(jl;?XE6v*Y+3jJa*$6*}JuEkMV+)2kJ-|xT z3qO}VtTcV_UF~6|*#*x#mX!-unnCz^>|v!DjGwpmz(I))Vwle!*1YDS`1zZxG{@o> zu!oiAB>aN*u+p4`U&#Kfnjz774Bb4yO4Nd1*dA7zEAhN9Te)DRxenjm9#)#0@s&NS z(zJg4-NCS^2Uv;j$1i3NE6sTP;`Xr8JcE}#tTZp-d2_gO!AkS`V&&skOL{1W@*4)` zx4B`dCM(T{_@(V(rTGlMj6JM0-{P0Ghn41M{Brp6`EyXBKNyzx4X|3HM&n<64|`Z? zW?H=X73^W9>4aa=9#)z;@hjQGsuRt}u(AhOi59`HVh<~g@T=OxO0yh(HG5cTR>iMw z4=YXGlYuwoD;KOp8{pTphm~eC{95*~(rk#xB|v#8!Eb60E6tVo&Fo>(@cCcMu(=0ViEhGgVGk?KIQ*9Ou+rRv-^w0VnuqaQ+rvup zWZ}Bn#sdc>dX5{mwTG4FReUddSZUtEZ)XoH&HMQ6?O~;vir=Bt*9{Iz^fkkdz5$k_ z#!vX(_OR0Yj_+d+E6sF#Uwc?-+Nt=R>|yn-edxfjbCZ>5c6>j3SZU_P_qT_YrW<~M zJ*+f~;|JQqO0!JOu#1Of7!s|7-_;&gnlh-+v3UQJe-UGaO{!%DLkejj^SX@=nUwJ%(L{-5YjhW~hgmFOt^V0&0; zj>qq34=c^7_#yVN(wu|e-yT+LQ@;N$U^u`7tVEaL544As=4$*w_OR02fIrwCR+?M! zhuFhPb0@w&)B^`4x}V`Ndst~6#UE}DE6r2*BkWzDv9#)!(_+j=n2PK-!aHI!V ziKgI>vWJysD*kADSZTh-A7c+I%}@Aa?O`30*Z%vOY z1%JGISZU_RpI{Fw&0nH4|U9#(1i`fGGyxW)slMDyUUwTG2v zA^dgru+l7wzuq2Jnx*hJ*uzTGW6ARIs~bI(L-`E@^J?5MrpZdP4*n*4SZOxG-)s*n z%@+7u>|v$pg})VFK7S5M)Q4fLZ-CVrmG_9l-)0Xh%^>_Zdst}(<8QZzmF6J)9rm#5 zL_-rA|SZV5u8Se7{E72(Y{r0fZT#tXi z9#)!L@ekU=N^>XvA$wS9?#Dlz*MAO5^eDq4z5&+5=BMzFHd$$&$B(y%mF89aWA?Dp zyoG<<{;eg;kG~Rq!0?0zScyKzKWPsu&G-1H>|v$(4ga(~tTcb)C)mTH;rnlfrONP( z2Uv+_#XoBgD@_;tbM~;(%!7a49#)!e_!sP9rCF?SUA^dmgAy&x4KLZlO0zQlWqVj@ z*2KSJ4=c@j_*d;=rP&n!TC1-c9F%BlhKarbmZQdw_}A@WrRj%%!yZH0R>qv4@rBBK*7du+ogG8Q$|S ziXqYU`1kE$r5T5xVh=0LL--HuVWpXX|Ii**nwRh&<@KL~5=~(5!%Fi5{!@EcX{O;nvoBnJ{-3DL(q;JE1FS?H@n6`(N;4<^OM6&p7Q%mJ4=c@* z_^<6@wKnDZuLr|79$+O}6aTF}tTY?rzq5yxW-I*n_OR0Q!T(?nE6o6W{i6pCO0);V zPxi3V48i|w4=c@3{4e&f(j15X)gD%wlkvaV*Bq4SOonM5U?m!f|J@!|nil*Y_OQ~7 z#{X##E6uI=zwBZCnb-fj82D<~V#;dst~s z$Io4R;Gjh3GR$KSYi{#Jc%CXzE?8+U$IoXEE6r&9{PwWY+=ySm{>GXi(O8BBJ-|wI z7k(joSZN->^Eiug!Adh8zpy>5G*9Cfv4>R}zWy37Fm(3-E75EC${tplckzqb!%FiR zeldGkX}-lTZVxNX&&!mLU-8_Ha+O2*4FmH(+^|HGm1gE;i(k?nR+`!HOWDIp(-pt8 zJ*+efO}n+ zR`dWX(H{7f>|v!Df?wGlR+^#sRqSD|v!@0Kb<#tTf&6d)pUYul$CA4@)rY(_|%D4!^HGtTd|@ZnMmNr*78a)EPTH*0#}D z-LEB$$L^bN>kdD+YkTs>EuDAhux;DBH*VRLe?Gf;%WbXaEnBAV&|zrXMYd`gx?}n0 zm>pZb+p+Bb>D{t!@AA(T`nHVVpEK{@GNpIf*VwZNFc6M~$=F zwcOmNL)E@;K9tB5(U&`Ay{XB3u98XmdgL+vaRp zduRU(w%z%E{;g+6UiH87y~yv1{GWfpvi-`x;E!%C>-H=Eg1_@QAB!x95&tV+jGR(r z`@I$}e{Ox_mRb9k|J6P`r)^{BEn8M9+-`}z$^+P1x)10uXUqQmJ9KGx>)=M?sFoA@ zm;a&HnB2DU%_=Q7m#ppLm$tmzzkI;OI<@@9Kd-;ErQ3k=&lzWH*|hw#-Ah-MzsKEj z$bfR&f(y*hIP#p9%LbH>G|Nkk#+!4uJW;sqlus@gamU;(e-9}CuXdi=(s^LHt;^K% z*Lz#m9N1yewik|R*|n&)Uwn4K@^`B98lRURxUU>fa TWxt+ntG3IIZJ9o>!`%M|!azv( diff --git a/components/vision-ui/vision_ui_lib.h b/components/vision-ui/vision_ui_lib.h index c572586..2d065c1 100644 --- a/components/vision-ui/vision_ui_lib.h +++ b/components/vision-ui/vision_ui_lib.h @@ -103,13 +103,13 @@ typedef struct vision_ui_list_icon { // NOLINT size_t footer_width; // NOLINT size_t footer_height; // NOLINT -} vision_ui_icon_t; // NOLINT +} vision_ui_icon_t; typedef struct vision_ui_font { // NOLINT const void* font; // NOLINT int8_t top_compensation; // NOLINT int8_t bottom_compensation; // NOLINT -} vision_ui_font_t; // NOLINT +} vision_ui_font_t; typedef enum vision_ui_action_t { // NOLINT UiActionNone, @@ -117,7 +117,7 @@ typedef enum vision_ui_action_t { // NOLINT UiActionGoNext, UiActionEnter, UiActionExit, -} vision_ui_action_t; // NOLINT +} vision_ui_action_t; #else #include "vision_ui_item.h" #include "vision_ui_renderer.h" @@ -139,9 +139,9 @@ typedef struct LumenSystemConfig { const uint8_t* usbIcon; const uint8_t* statIcon; const uint8_t* creeperIcon; + const uint8_t* minecraftSyncIcon; } LumenSystemConfig; - LumenSystemInfo lumenGetSystemInfo(); LumenSystemConfig lumenGetSystemConfig(); @@ -183,6 +183,14 @@ typedef struct LumenEasterEggState { LumenEasterEgg lumenGetEasterEgg(); LumenEasterEggState lumenGetEasterEggState(); +typedef struct LumenMinecraftSync { + void (*initFunction)(); + void (*loopFunction)(); + void (*exitFunction)(); +} LumenMinecraftSync; + +LumenMinecraftSync lumenGetMinecraftSync(); + extern void vision_ui_step_render(); // NOLINT extern void vision_ui_allocator_set(void* (*allocator)(vision_alloc_op_t op, size_t size, size_t count, void* ptr) ); // NOLINT diff --git a/dependencies.lock b/dependencies.lock index 6094fd2..f872ae1 100644 --- a/dependencies.lock +++ b/dependencies.lock @@ -1,6 +1,6 @@ dependencies: espp/base_component: - component_hash: a4da2e51f8545f3f56c2cc4df62d58a5c1683aae6dca1303da6eeb382735d916 + component_hash: 569d8a4c2520fbb8f9c79ddd772e278506c80099e218599c602a8891bb995bf9 dependencies: - name: espp/logger registry_url: https://components.espressif.com @@ -12,9 +12,9 @@ dependencies: source: registry_url: https://components.espressif.com type: service - version: 1.0.31 + version: 1.0.33 espp/base_peripheral: - component_hash: a4c8029460ae556c256feb44d371bb6d9a47b9d0e7f862abf8ea2a312490b46e + component_hash: e9d1f27fa9346541b76aebb7dbf6150284cc600ac8019e208f600f6afac49990 dependencies: - name: espp/base_component registry_url: https://components.espressif.com @@ -26,7 +26,7 @@ dependencies: source: registry_url: https://components.espressif.com type: service - version: 1.0.31 + version: 1.0.33 espp/filters: component_hash: 5f680ee0759e546157ecf3140dcb22122cf27136acc745e3e404f4a9cdf01dbc dependencies: @@ -50,7 +50,7 @@ dependencies: type: service version: 1.0.31 espp/format: - component_hash: 97ebfaceff6189ab95190bee4da489c48e5295df8ddfcd401eabae88fead8bb8 + component_hash: 9723eeb2cadb7191255337320f51edec7b300c648d98aa4ab92e93365297acc4 dependencies: - name: idf require: private @@ -58,9 +58,9 @@ dependencies: source: registry_url: https://components.espressif.com type: service - version: 1.0.31 + version: 1.0.33 espp/logger: - component_hash: 4a0c402511864c995d9261642fb545c1af2a3fb6ff994007ce88f08155595b88 + component_hash: 91bc16342ba44200df5b56977270921a863e3447d3c94049a75e30b8d515b34d dependencies: - name: espp/format registry_url: https://components.espressif.com @@ -72,7 +72,7 @@ dependencies: source: registry_url: https://components.espressif.com type: service - version: 1.0.31 + version: 1.0.33 espp/lsm6dso: component_hash: 9fe231a62fa2d7181ae9d42f37f3929c7c42c6c8b010ba7164d97bff88fe357a dependencies: @@ -92,7 +92,7 @@ dependencies: type: service version: 1.0.31 espp/math: - component_hash: f44d40df31c2090ac56ce8e13073192563eeae259b6feb8f77084f39db1a9b29 + component_hash: e17d2f738174843a4e0ccc6d45c161e56077643f063484f184f645e469a52faa dependencies: - name: espp/format registry_url: https://components.espressif.com @@ -104,7 +104,17 @@ dependencies: source: registry_url: https://components.espressif.com type: service - version: 1.0.31 + version: 1.0.33 + espressif/cjson: + component_hash: 9372811fb197926f522c467627cf4a8e72b681e0366e17879631da801103aef3 + dependencies: + - name: idf + require: private + version: '>=5.0' + source: + registry_url: https://components.espressif.com/ + type: service + version: 1.7.19 espressif/esp-dsp: component_hash: 42dce32d46ac93dc11f60d368e29a830e9661c7345d794b8a45c343479cae636 dependencies: @@ -122,7 +132,8 @@ dependencies: direct_dependencies: - espp/filters - espp/lsm6dso +- espressif/cjson - idf -manifest_hash: 6cdf6f27fb1588c9d3cefad60614ba74add9b45ba0cbbc9dcfa9f50584405828 +manifest_hash: aecbf1075f1692d244f2dae5ffccd5f7ba41d52f3346cb6ed99ec47b8e40c136 target: esp32c3 version: 2.0.0 diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index c7597db..ec7ca08 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -13,6 +13,7 @@ idf_component_register( esp_driver_i2c esp_driver_spi esp_lcd + cjson ina226 ) diff --git a/main/idf_component.yml b/main/idf_component.yml index eda4d74..ceaabdb 100644 --- a/main/idf_component.yml +++ b/main/idf_component.yml @@ -16,3 +16,4 @@ dependencies: # public: true espp/lsm6dso: ^1.0.31 espp/filters: ^1.0.31 + espressif/cjson: ^1.7.19 diff --git a/main/include/display.hpp b/main/include/display.hpp index 02fbddf..d1528ab 100644 --- a/main/include/display.hpp +++ b/main/include/display.hpp @@ -22,9 +22,20 @@ along with this program. If not, see . #include +#define LCD_H_RES 240 +#define LCD_V_RES 240 + extern void displayFrameRender(); extern void displayInit(vision_ui_action_t (*callback)()); +extern void displayDriverExtensionRGBBitmapDraw( + int16_t x, + int16_t y, + int16_t width, + int16_t height, + const uint16_t* colorData +); + #endif // MAIN_INCLUDE_DISPLAY_HPP diff --git a/main/include/serial_pack.hpp b/main/include/serial_pack.hpp new file mode 100644 index 0000000..371c629 --- /dev/null +++ b/main/include/serial_pack.hpp @@ -0,0 +1,38 @@ +/* +Lumen +Copyright (C) 2025 Finn Sheng + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ +#pragma once + +#ifndef MAIN_INCLUDE_SERIAL_PACK_HPP +#define MAIN_INCLUDE_SERIAL_PACK_HPP + +#include +#include + +using SerialPackHandler = void (*)(const uint8_t* data, size_t currentSize); + +// Initialize USB Serial/TAG driver and internal handler table. Safe to call multiple times. +extern void serialPackInit(); +// Start the FreeRTOS task that parses incoming serial packs. +extern void serialPackStart(); +// Stop the parsing task (driver remains initialized). +extern void serialPackStop(); + +// Register or replace a handler for a given path. Call after init and before start. +extern void serialPackAttachHandler(const char* path, SerialPackHandler handler); + +#endif // MAIN_INCLUDE_SERIAL_PACK_HPP diff --git a/main/src/serial_pack.cpp b/main/src/serial_pack.cpp new file mode 100644 index 0000000..b8c8d1d --- /dev/null +++ b/main/src/serial_pack.cpp @@ -0,0 +1,293 @@ +/* +Lumen +Copyright (C) 2025 Finn Sheng + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#include "include/serial_pack.hpp" + +#include + +#include +#include + +#include +#include +#include +#include + + +constexpr char SERIAL_PACK_TAG[] = "[lumen:serial_pack]"; +constexpr size_t K_MAX_HANDLERS = 2; +constexpr size_t K_MAX_PATH_LEN = 16; +constexpr size_t K_MAX_DATA_LEN = 1024 * 2; +constexpr int64_t K_RX_TIMEOUT_US = 3 * 1000 * 1000; + +struct HandlerEntry { + char path[K_MAX_PATH_LEN]; + SerialPackHandler handler; +}; + +HandlerEntry S_HANDLERS[K_MAX_HANDLERS] = {}; +size_t S_HANDLER_COUNT = 0; + +TaskHandle_t S_SERIAL_TASK = nullptr; +volatile bool S_RUNNING = false; +bool S_INITIALIZED = false; + +void logUnhandledData(const char* path, const uint8_t* data, const size_t len, const bool truncated) { + char hexPreview[3 * 16 + 1] = {}; + const size_t previewLen = len > 16 ? 16 : len; + for (size_t i = 0; i < previewLen; ++i) { + static constexpr char kHex[] = "0123456789ABCDEF"; + hexPreview[i * 3] = kHex[(data[i] >> 4) & 0x0F]; + hexPreview[i * 3 + 1] = kHex[data[i] & 0x0F]; + hexPreview[i * 3 + 2] = (i + 1 < previewLen) ? ' ' : '\0'; + } + ESP_LOGW( + SERIAL_PACK_TAG, + "unhandled path '%s', size=%u, data=%s%s", + path, + static_cast(len), + hexPreview, + truncated ? " ..." : "" + ); +} + +SerialPackHandler findHandler(const char* path) { + for (size_t i = 0; i < S_HANDLER_COUNT; ++i) { + if (strcmp(S_HANDLERS[i].path, path) == 0) { + return S_HANDLERS[i].handler; + } + } + return nullptr; +} + +void resetState(char* path, size_t& pathLen, uint8_t* data, size_t& dataLen, bool& inData) { + path[0] = '\0'; + pathLen = 0; + dataLen = 0; + inData = false; +} + +void logUnhandledPath(const char* path, const uint8_t* data, const size_t len, const bool truncated) { + if (len > 0) { + logUnhandledData(path, data, len, truncated); + } else { + ESP_LOGW(SERIAL_PACK_TAG, "unhandled path '%s', size=0", path); + } +} + +[[noreturn]] +void serialPackTask(void*) { + static char path[K_MAX_PATH_LEN] = {}; + size_t pathLen = 0; + static uint8_t data[K_MAX_DATA_LEN] = {}; + size_t dataLen = 0; + bool inData = false; + bool discardUntilNewline = false; + uint8_t sizeBytes[sizeof(uint32_t)] = {}; + size_t sizeIndex = 0; + uint32_t remaining = 0; + int64_t lastRxUs = esp_timer_get_time(); + + resetState(path, pathLen, data, dataLen, inData); + + auto handleByte = [&](const uint8_t byte) { + if (discardUntilNewline) { + if (byte == '\n') { + discardUntilNewline = false; + resetState(path, pathLen, data, dataLen, inData); + } + return; + } + + if (!inData) { + if (byte == '\r') { + return; + } + if (byte == '\n') { + if (pathLen == 0) { + return; + } + path[pathLen] = '\0'; + inData = true; + dataLen = 0; + sizeIndex = 0; + remaining = 0; + ESP_LOGD(SERIAL_PACK_TAG, "path is %s", path); + return; + } + + if (byte == ' ') { + ESP_LOGE(SERIAL_PACK_TAG, "invalid path: contains space"); + discardUntilNewline = true; + return; + } + + if (pathLen + 1 >= K_MAX_PATH_LEN) { + ESP_LOGE(SERIAL_PACK_TAG, "path too long"); + discardUntilNewline = true; + return; + } + + path[pathLen++] = static_cast(byte); + return; + } + + if (sizeIndex < sizeof(uint32_t)) { + sizeBytes[sizeIndex++] = byte; + if (sizeIndex < sizeof(uint32_t)) { + return; + } + + const uint32_t size = static_cast(sizeBytes[0]) | (static_cast(sizeBytes[1]) << 8U) | + (static_cast(sizeBytes[2]) << 16U) | + (static_cast(sizeBytes[3]) << 24U); + remaining = size; + + if (remaining == 0) { + if (const SerialPackHandler handler = findHandler(path)) { + handler(nullptr, 0); + } else { + logUnhandledPath(path, nullptr, 0, false); + } + resetState(path, pathLen, data, dataLen, inData); + } + return; + } + + data[dataLen++] = byte; + if (remaining > 0) { + --remaining; + } + + if (dataLen >= K_MAX_DATA_LEN || remaining == 0) { + if (const SerialPackHandler handler = findHandler(path)) { + if (dataLen > 0) { + handler(data, dataLen); + } + } else { + logUnhandledData(path, data, dataLen, remaining > 0); + } + dataLen = 0; + } + + if (remaining == 0) { + if (const SerialPackHandler handler = findHandler(path)) { + handler(nullptr, 0); + } else { + logUnhandledPath(path, nullptr, 0, false); + } + resetState(path, pathLen, data, dataLen, inData); + } + }; + + static uint8_t rx[128] = {}; + while (S_RUNNING) { + const int read = usb_serial_jtag_read_bytes(rx, sizeof(rx), pdMS_TO_TICKS(20)); + if (!S_RUNNING) { + break; + } + if (read <= 0) { + if (inData && sizeIndex >= sizeof(uint32_t) && remaining > 0) { + const int64_t now = esp_timer_get_time(); + if (now - lastRxUs > K_RX_TIMEOUT_US) { + ESP_LOGW(SERIAL_PACK_TAG, "rx timeout, aborting pack"); + resetState(path, pathLen, data, dataLen, inData); + discardUntilNewline = false; + sizeIndex = 0; + remaining = 0; + lastRxUs = now; + } + } + continue; + } + lastRxUs = esp_timer_get_time(); + for (int i = 0; i < read; ++i) { + handleByte(rx[i]); + } + } + + S_SERIAL_TASK = nullptr; + vTaskDelete(nullptr); + while (true) { + } +} + + +void serialPackInit() { + if (S_INITIALIZED) { + return; + } + + usb_serial_jtag_driver_config_t cfg = { + .tx_buffer_size = 1024, + .rx_buffer_size = 1024 * 16, + }; + if (const esp_err_t err = usb_serial_jtag_driver_install(&cfg); err != ESP_OK) { + ESP_LOGE(SERIAL_PACK_TAG, "usb_serial_jtag_driver_install failed: %s", esp_err_to_name(err)); + return; + } + + S_INITIALIZED = true; +} + +void serialPackStart() { + if (!S_INITIALIZED) { + serialPackInit(); + } + + if (!S_INITIALIZED || S_RUNNING) { + return; + } + + S_RUNNING = true; + xTaskCreate(serialPackTask, "serial_pack", 1024 * 2, nullptr, 6, &S_SERIAL_TASK); +} + +void serialPackStop() { + if (!S_RUNNING) { + return; + } + + S_RUNNING = false; +} + +void serialPackAttachHandler(const char* path, const SerialPackHandler handler) { + if (!path || !handler) { + ESP_LOGE(SERIAL_PACK_TAG, "invalid handler registration"); + return; + } + + for (size_t i = 0; i < S_HANDLER_COUNT; ++i) { + if (strcmp(S_HANDLERS[i].path, path) == 0) { + S_HANDLERS[i].handler = handler; + return; + } + } + + if (S_HANDLER_COUNT >= K_MAX_HANDLERS) { + ESP_LOGE(SERIAL_PACK_TAG, "handler table full"); + return; + } + + ESP_LOGI(SERIAL_PACK_TAG, "handler %s is attached", path); + + std::strncpy(S_HANDLERS[S_HANDLER_COUNT].path, path, K_MAX_PATH_LEN - 1); + S_HANDLERS[S_HANDLER_COUNT].path[K_MAX_PATH_LEN - 1] = '\0'; + S_HANDLERS[S_HANDLER_COUNT].handler = handler; + ++S_HANDLER_COUNT; +} diff --git a/main/src/ui_assets_provider.cpp b/main/src/ui_assets_provider.cpp index 996e433..2743d63 100644 --- a/main/src/ui_assets_provider.cpp +++ b/main/src/ui_assets_provider.cpp @@ -1,18 +1,25 @@ #include +#include +#include #include #include #include +#include #include +#include + #include #include #include "include/buzzer.hpp" #include "include/current_sensor.hpp" +#include "include/display.hpp" #include "include/efuse.hpp" #include "include/motion.hpp" +#include "include/serial_pack.hpp" // 'logo', 240x240px // 'logo', 240x240px @@ -800,6 +807,82 @@ static uint8_t CREEPER_ICON[] = { 0x00, 0x00, 0x00, 0x00, }; +static uint8_t MINECRAFT_SYNC_ICON[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, + 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0xE0, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, + 0x07, 0x80, 0xFF, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x01, 0x00, 0xFF, 0x03, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0xFC, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x3E, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, + 0xE0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x3E, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xE0, 0x03, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x00, + 0x00, 0xE0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x3E, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xE0, + 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, + 0x01, 0x00, 0xFC, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x07, 0x00, 0xFF, 0x03, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x0F, 0xC0, 0xFF, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xF0, 0x3F, 0xF0, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFC, + 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, + 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, + 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, + 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, + 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, + 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, + 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, + 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0xFF, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, +}; + constexpr uint8_t U8G2_FONT_SFCOMPACT_DISPLAY_SEMIBOLD_42_ASCII[5355] U8G2_FONT_SECTION( "u8g2_font_sfcompact_display_semibold_42_ascii" ) = "_\0\5\5\6\6\4\7\7\63\66\376\365'\365)\367\7J\16k\24\316 \6\0\200\300%!\27\10" @@ -1287,6 +1370,7 @@ LumenSystemConfig lumenGetSystemConfig() { .usbIcon = USB_ICON, .statIcon = STAT_ICON, .creeperIcon = CREEPER_ICON, + .minecraftSyncIcon = MINECRAFT_SYNC_ICON, }; return config; } @@ -1924,3 +2008,193 @@ LumenEasterEggState lumenGetEasterEggState() { .ignite = igniteState, }; } + +namespace { + constexpr size_t K_MINECRAFT_SYNC_JSON_MAX = 1024; + constexpr size_t K_MINECRAFT_SYNC_SKIN_MAX = LCD_H_RES * LCD_V_RES * 2 + 4; + + struct MinecraftSyncState { + std::string mode = "-----"; + float health = 0.0F; + float maxHealth = 0.0F; + bool hasState = false; + bool serialAttached = false; + bool jsonOverflow = false; + bool skinOverflow = false; + uint16_t skinWidth = 0; + uint16_t skinHeight = 0; + bool skinReady = false; + std::vector jsonBuffer; + std::vector skinBuffer; + std::vector skinPixels; + }; + + MinecraftSyncState S_MINECRAFT_SYNC; + + uint16_t readU16Le(const uint8_t* data) { + return static_cast(data[0] | (static_cast(data[1]) << 8)); + } + + float parseJsonFloat(const cJSON* item, const float fallback) { + if (cJSON_IsNumber(item)) { + return static_cast(item->valuedouble); + } + if (cJSON_IsString(item) && item->valuestring) { + return static_cast(std::strtof(item->valuestring, nullptr)); + } + return fallback; + } + + void minecraftSyncJsonHandler(const uint8_t* data, const size_t size) { + if (data && size > 0) { + if (S_MINECRAFT_SYNC.jsonBuffer.size() + size > K_MINECRAFT_SYNC_JSON_MAX) { + S_MINECRAFT_SYNC.jsonOverflow = true; + S_MINECRAFT_SYNC.jsonBuffer.clear(); + return; + } + S_MINECRAFT_SYNC.jsonBuffer.insert(S_MINECRAFT_SYNC.jsonBuffer.end(), data, data + size); + return; + } + + if (S_MINECRAFT_SYNC.jsonOverflow) { + S_MINECRAFT_SYNC.jsonOverflow = false; + return; + } + + if (S_MINECRAFT_SYNC.jsonBuffer.empty()) { + return; + } + + cJSON* root = cJSON_ParseWithLength( + reinterpret_cast(S_MINECRAFT_SYNC.jsonBuffer.data()), S_MINECRAFT_SYNC.jsonBuffer.size() + ); + S_MINECRAFT_SYNC.jsonBuffer.clear(); + if (!root) { + return; + } + + if (const cJSON* mode = cJSON_GetObjectItem(root, "mode"); cJSON_IsString(mode) && mode->valuestring) { + S_MINECRAFT_SYNC.mode = mode->valuestring; + } + + S_MINECRAFT_SYNC.health = parseJsonFloat(cJSON_GetObjectItem(root, "health"), S_MINECRAFT_SYNC.health); + S_MINECRAFT_SYNC.maxHealth = + parseJsonFloat(cJSON_GetObjectItem(root, "max_health"), S_MINECRAFT_SYNC.maxHealth); + S_MINECRAFT_SYNC.hasState = true; + + cJSON_Delete(root); + } + + void minecraftSyncSkinHandler(const uint8_t* data, const size_t size) { + if (data && size > 0) { + if (S_MINECRAFT_SYNC.skinReady) { + S_MINECRAFT_SYNC.skinReady = false; + S_MINECRAFT_SYNC.skinBuffer.clear(); + } + if (S_MINECRAFT_SYNC.skinBuffer.size() + size > K_MINECRAFT_SYNC_SKIN_MAX) { + S_MINECRAFT_SYNC.skinOverflow = true; + S_MINECRAFT_SYNC.skinBuffer.clear(); + return; + } + S_MINECRAFT_SYNC.skinBuffer.insert(S_MINECRAFT_SYNC.skinBuffer.end(), data, data + size); + return; + } + + if (S_MINECRAFT_SYNC.skinOverflow) { + S_MINECRAFT_SYNC.skinOverflow = false; + return; + } + + if (S_MINECRAFT_SYNC.skinBuffer.size() < 4) { + S_MINECRAFT_SYNC.skinBuffer.clear(); + S_MINECRAFT_SYNC.skinReady = false; + return; + } + + const uint16_t width = readU16Le(S_MINECRAFT_SYNC.skinBuffer.data()); + const uint16_t height = readU16Le(S_MINECRAFT_SYNC.skinBuffer.data() + 2); + const size_t pixelCount = static_cast(width) * static_cast(height); + const size_t expectedBytes = pixelCount * 2; + + if (const size_t payloadBytes = S_MINECRAFT_SYNC.skinBuffer.size() - 4; + pixelCount == 0 || payloadBytes < expectedBytes) { + S_MINECRAFT_SYNC.skinBuffer.clear(); + S_MINECRAFT_SYNC.skinReady = false; + return; + } + + S_MINECRAFT_SYNC.skinPixels.assign(pixelCount, 0); + const uint8_t* pixelData = S_MINECRAFT_SYNC.skinBuffer.data() + 4; + for (size_t i = 0; i < pixelCount; ++i) { + S_MINECRAFT_SYNC.skinPixels[i] = readU16Le(pixelData + i * 2); + } + S_MINECRAFT_SYNC.skinWidth = width; + S_MINECRAFT_SYNC.skinHeight = height; + S_MINECRAFT_SYNC.skinReady = true; + S_MINECRAFT_SYNC.skinBuffer.clear(); + } + + void minecraftSyncDraw() { + static constexpr auto skinY = 20; + static constexpr auto textY = LCD_V_RES - 20; + if (!S_MINECRAFT_SYNC.hasState && !S_MINECRAFT_SYNC.skinReady) { + return; + } + + if (S_MINECRAFT_SYNC.skinReady && !S_MINECRAFT_SYNC.skinPixels.empty()) { + const int16_t skinX = static_cast((LCD_H_RES - S_MINECRAFT_SYNC.skinWidth) / 2); + displayDriverExtensionRGBBitmapDraw( + skinX, + skinY, + static_cast(S_MINECRAFT_SYNC.skinWidth), + static_cast(S_MINECRAFT_SYNC.skinHeight), + S_MINECRAFT_SYNC.skinPixels.data() + ); + } + + if (S_MINECRAFT_SYNC.hasState) { + char modeLine[32] = {}; + char healthLine[32] = {}; + + std::snprintf(modeLine, sizeof(modeLine), "%s", S_MINECRAFT_SYNC.mode.c_str()); + std::snprintf( + healthLine, + sizeof(healthLine), + "%.1f/%.1f", + static_cast(S_MINECRAFT_SYNC.health), + static_cast(S_MINECRAFT_SYNC.maxHealth) + ); + + const uint16_t modeWidth = vision_ui_driver_str_width_get(modeLine); + const uint16_t healthWidth = vision_ui_driver_str_width_get(healthLine); + const uint16_t lineHeight = vision_ui_driver_str_height_get(); + + const int16_t modeX = static_cast((LCD_H_RES - modeWidth) / 2); + const int16_t healthX = static_cast((LCD_H_RES - healthWidth) / 2); + static constexpr int16_t healthY = textY; + const int16_t modeY = static_cast(healthY - lineHeight - 2); + + vision_ui_driver_str_draw(static_cast(modeX), static_cast(modeY), modeLine); + vision_ui_driver_str_draw(static_cast(healthX), healthY, healthLine); + } + } +} // namespace + +LumenMinecraftSync lumenGetMinecraftSync() { + return { + .initFunction = + []() { + if (!S_MINECRAFT_SYNC.serialAttached) { + serialPackAttachHandler("sync", minecraftSyncJsonHandler); + serialPackAttachHandler("sync/skin", minecraftSyncSkinHandler); + S_MINECRAFT_SYNC.skinBuffer.reserve(1024 * 10); + S_MINECRAFT_SYNC.serialAttached = true; + } + const auto config = lumenGetSystemConfig(); + vision_ui_driver_font_set(config.normal); + serialPackStart(); + }, + .loopFunction = []() { minecraftSyncDraw(); }, + .exitFunction = []() { serialPackStop(); }, + }; +} diff --git a/main/src/ui_hardware_driver.cpp b/main/src/ui_hardware_driver.cpp index a5ff369..8a5367c 100644 --- a/main/src/ui_hardware_driver.cpp +++ b/main/src/ui_hardware_driver.cpp @@ -44,9 +44,6 @@ along with this program. If not, see . // DMA block lines (must divide V_RES) #define PARALLEL_LINES 128 -#define LCD_H_RES 240 -#define LCD_V_RES 240 - u8g2_t U8G2; uint8_t G_U8G2_BUF[LCD_H_RES * LCD_V_RES / 8]; @@ -102,6 +99,28 @@ static bool onColorTransDone(esp_lcd_panel_io_handle_t, esp_lcd_panel_io_event_d return false; } +static constexpr uint16_t rgb565(const uint8_t r, const uint8_t g, const uint8_t b) { + return static_cast(((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)); +} + +static constexpr uint16_t U8G2_COLOR_OFF = rgb565(0, 0, 0); +static constexpr uint16_t U8G2_COLOR_ON = rgb565(255, 255, 255); + +static void displayPrepareRGBBuffers() { + static constexpr int bufSize = sizeof(S_LINES) / sizeof(S_LINES[0]); + constexpr int neededBuffers = (LCD_V_RES + PARALLEL_LINES - 1) / PARALLEL_LINES; + constexpr int buffersToClear = std::min(bufSize, neededBuffers); + for (int i = 0; i < buffersToClear; ++i) { + if (!S_LINES[i]) { + continue; + } + while (S_BUF_BUSY[i]) { + taskYIELD(); + } + std::fill_n(S_LINES[i], LCD_H_RES * PARALLEL_LINES, U8G2_COLOR_OFF); + } +} + static bool DISPLAY_READY = false; void displayFrameRender() { @@ -111,6 +130,7 @@ void displayFrameRender() { const uint32_t start = esp_timer_get_time(); vision_ui_driver_buffer_clear(); + displayPrepareRGBBuffers(); vision_ui_step_render(); const uint32_t flash = esp_timer_get_time(); @@ -243,13 +263,6 @@ void displayInit(vision_ui_action_t (*callback)()) { DISPLAY_READY = true; } -static constexpr uint16_t rgb565(const uint8_t r, const uint8_t g, const uint8_t b) { - return static_cast(((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)); -} - -static constexpr uint16_t U8G2_COLOR_OFF = rgb565(0, 0, 0); -static constexpr uint16_t U8G2_COLOR_ON = rgb565(255, 255, 255); - static uint16_t MONO_TO_RGB565[256][8]; static bool LUT_READY = false; @@ -292,8 +305,10 @@ void vision_ui_driver_buffer_send() { uint16_t* row = block + line * LCD_H_RES; for (int dstX = 0; dstX < LCD_H_RES; ++dstX) { - const int srcX = (LCD_H_RES - 1) - dstX; - row[dstX] = (rowPtr[srcX] & bitMask) ? U8G2_COLOR_ON : U8G2_COLOR_OFF; + // row[dstX] = (rowPtr[srcX] & bitMask) ? U8G2_COLOR_ON : U8G2_COLOR_OFF; + if (const int srcX = (LCD_H_RES - 1) - dstX; rowPtr[srcX] & bitMask) { + row[dstX] = U8G2_COLOR_ON; + } } } @@ -303,6 +318,54 @@ void vision_ui_driver_buffer_send() { } } +void displayDriverExtensionRGBBitmapDraw( + const int16_t x, + const int16_t y, + const int16_t width, + const int16_t height, + const uint16_t* colorData +) { + if (!colorData || width <= 0 || height <= 0) { + return; + } + + const int16_t x0 = std::max(0, x); + const int16_t y0 = std::max(0, y); + const int16_t x1 = std::min(LCD_H_RES, x + width); + const int16_t y1 = std::min(LCD_V_RES, y + height); + if (x0 >= x1 || y0 >= y1) { + return; + } + + static constexpr int bufSize = sizeof(S_LINES) / sizeof(S_LINES[0]); + bool waited[bufSize] = {false}; + + for (int srcY = y0; srcY < y1; ++srcY) { + const int inY = srcY - y; + const int dstY = (LCD_V_RES - 1) - srcY; + const int bufIdx = dstY / PARALLEL_LINES; + if (bufIdx < 0 || bufIdx >= bufSize || !S_LINES[bufIdx]) { + continue; + } + if (!waited[bufIdx]) { + while (S_BUF_BUSY[bufIdx]) { + taskYIELD(); + } + waited[bufIdx] = true; + } + + const int bufYStart = bufIdx * PARALLEL_LINES; + const int rowOffset = (dstY - bufYStart) * LCD_H_RES; + + for (int srcX = x0; srcX < x1; ++srcX) { + const int inX = srcX - x; + const int dstX = (LCD_H_RES - 1) - srcX; + const uint16_t pixel = colorData[inY * width + inX]; + S_LINES[bufIdx][rowOffset + dstX] = pixel; + } + } +} + void* vision_ui_driver_buffer_pointer_get() { return G_U8G2_BUF; } diff --git a/script/display_skin_payload.py b/script/display_skin_payload.py new file mode 100644 index 0000000..83aa97c --- /dev/null +++ b/script/display_skin_payload.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +import argparse +import sys +from pathlib import Path + +try: + from PIL import Image +except ImportError as exc: + raise SystemExit("Pillow is required: pip install pillow") from exc + + +def rgb565_to_rgb888_be(data: bytes, width: int, height: int) -> Image.Image: + expected = width * height * 2 + if len(data) < expected: + raise ValueError(f"payload too small: {len(data)} < {expected}") + if len(data) > expected: + data = data[:expected] + + rgb = bytearray(width * height * 3) + dst = 0 + for i in range(0, expected, 2): + value = (data[i] << 8) | data[i + 1] # big-endian RGB565 + r = (value >> 11) & 0x1F + g = (value >> 5) & 0x3F + b = value & 0x1F + rgb[dst] = (r << 3) | (r >> 2) + rgb[dst + 1] = (g << 2) | (g >> 4) + rgb[dst + 2] = (b << 3) | (b >> 2) + dst += 3 + + return Image.frombytes("RGB", (width, height), bytes(rgb)) + + +def main() -> int: + parser = argparse.ArgumentParser(description="Display an RGB565 big-endian payload.") + parser.add_argument("file", help="Binary payload file") + parser.add_argument("--width", type=int, required=True, help="Image width") + parser.add_argument("--height", type=int, required=True, help="Image height") + parser.add_argument("--out", help="Output PNG path (optional)") + args = parser.parse_args() + + path = Path(args.file) + if not path.exists(): + print(f"file not found: {path}", file=sys.stderr) + return 2 + + blob = path.read_bytes() + data = blob[4:] # skip width/height header + image = rgb565_to_rgb888_be(data, args.width, args.height) + + if args.out: + image.save(args.out) + else: + image.show() + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/script/serial_pack_send.py b/script/serial_pack_send.py new file mode 100644 index 0000000..6ee27d5 --- /dev/null +++ b/script/serial_pack_send.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +import argparse +import struct +import sys +from pathlib import Path + +import serial + + +def main() -> int: + parser = argparse.ArgumentParser(description="Send a serial pack over USB Serial/JTAG.") + parser.add_argument("--port", default="/dev/cu.usbmodem1101", help="Serial device path") + parser.add_argument("--path", default="sync", help="Pack path (no spaces)") + parser.add_argument("--data", help="Payload string") + parser.add_argument("--file", help="Binary payload file") + parser.add_argument("--baud", type=int, default=460800, help="Baud rate") + args = parser.parse_args() + + path_bytes = args.path.encode("ascii") + if b" " in path_bytes or b"\n" in path_bytes: + print("path must not contain spaces or newlines", file=sys.stderr) + return 2 + + if args.data is None and args.file is None: + print("must provide --data or --file", file=sys.stderr) + return 2 + if args.data is not None and args.file is not None: + print("use only one of --data or --file", file=sys.stderr) + return 2 + + if args.file is not None: + file_path = Path(args.file) + if not file_path.exists(): + print(f"file not found: {file_path}", file=sys.stderr) + return 2 + data_bytes = file_path.read_bytes() + else: + data_bytes = args.data.encode("utf-8") + pkt = path_bytes + b"\n" + struct.pack("Xh{e*tAW+LYTO zr6Td(Icq(<(Q z`=f0}K{kik^J|?@~ zc-K#8euk%z)$#g#wi!meT0!UTxf>>h3E{wejZNTwldsa;&JW>8htQh?Xa===z^| z_vVW2L&eTFg=Tl=ipfU>-51AUFiTg6us2U5vnRdUc%@IM^2ITlmFSG0d7VGKQNS?T z4TD*_^8VWEsPQ`WxSmlEMMITGyUfy6qBA~|g*A-VItU_8rdR1|yv{wG%=?>G(&|iQ z_2sS#&*>`#^=Q0|?i4di7nqjc&Qd)$UbG@h2deKs71U{+=|V$glw?*IwgNZy7ev+@ zuTuh5&@iVn*FN`Fvi-XT#Ffs;=OA6;<7o^c8AfGh$&J^kSWjnkCF6=RXAe&zUfnIn z%+e))K_s)9^A+~SD^0uS-~)8FXMFn*Wo^bIob2wC5wGpC2yyk?czLs&&gjbZP5t$S zi}xQCXh@gpG-AJ5ctE6XyiQwG>7#S}j)Gb~fMid|-dw)EJW${-UD}0{Aok0R*Qv<$ zTwiBg(VGMuBg6admy5gnRX6VbF^hOPp-$(g z%Deu~+02YLmcfbP$!Gl??JMX~WpGN1#=HC*=~pT?796;r_x$ZoEzjudqh- zbQaQRADt}|tIW!k(jy!8<15IG*J+Cy^>hX)My6KA4Z0uhFG%pj^;6|d|DLFT&eY1` zxhNiY&%wqUYFb}s(9!u0hR%(syVz8}Awn8AUZ)5yMCy3iibt`o)MAQwRxWfuM=_h< z01ph*jn^q))$pp7kNBT%NG@mdJBIMF$D@>wz!;Lt+5C?5|Hl1hJxckgA49sUoXzh@ zBles1DCMJm4C$_NHoxQKY_-W=KZGF7B!7rzx+^{SPREdI?= d<8_LlLOq=))8SF>s^2l3bu#rR<)g>r{R1%6KDPh> literal 0 HcmV?d00001 diff --git a/sdkconfig b/sdkconfig index c484387..2bb44ff 100644 --- a/sdkconfig +++ b/sdkconfig @@ -488,13 +488,13 @@ CONFIG_ESPTOOLPY_MONITOR_BAUD=115200 # # Partition Table # -CONFIG_PARTITION_TABLE_SINGLE_APP=y -# CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE is not set +# CONFIG_PARTITION_TABLE_SINGLE_APP is not set +CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y # CONFIG_PARTITION_TABLE_TWO_OTA is not set # CONFIG_PARTITION_TABLE_TWO_OTA_LARGE is not set # CONFIG_PARTITION_TABLE_CUSTOM is not set CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" -CONFIG_PARTITION_TABLE_FILENAME="partitions_singleapp.csv" +CONFIG_PARTITION_TABLE_FILENAME="partitions_singleapp_large.csv" CONFIG_PARTITION_TABLE_OFFSET=0x8000 CONFIG_PARTITION_TABLE_MD5=y # end of Partition Table @@ -540,35 +540,16 @@ CONFIG_COMPILER_ORPHAN_SECTIONS_WARNING=y # # -# Application Level Tracing +# !!! MINIMAL_BUILD is enabled !!! # -# CONFIG_APPTRACE_DEST_JTAG is not set -CONFIG_APPTRACE_DEST_NONE=y -# CONFIG_APPTRACE_DEST_UART0 is not set -# CONFIG_APPTRACE_DEST_UART1 is not set -# CONFIG_APPTRACE_DEST_USB_CDC is not set -CONFIG_APPTRACE_DEST_UART_NONE=y -CONFIG_APPTRACE_UART_TASK_PRIO=1 -CONFIG_APPTRACE_LOCK_ENABLE=y -# end of Application Level Tracing # -# Bluetooth +# Only common components and those transitively required by the main component are listed # -# CONFIG_BT_ENABLED is not set # -# Common Options +# If a component configuration is missing, please add it to the main component's requirements # -# CONFIG_BT_BLE_LOG_SPI_OUT_ENABLED is not set -# end of Common Options -# end of Bluetooth - -# -# Console Library -# -# CONFIG_CONSOLE_SORTED_HELP is not set -# end of Console Library # # Driver Configurations @@ -644,38 +625,6 @@ CONFIG_TWAI_ERRATA_FIX_LISTEN_ONLY_DOM=y CONFIG_EFUSE_MAX_BLK_LEN=256 # end of eFuse Bit Manager -# -# ESP-TLS -# -CONFIG_ESP_TLS_USING_MBEDTLS=y -# CONFIG_ESP_TLS_USE_SECURE_ELEMENT is not set -CONFIG_ESP_TLS_USE_DS_PERIPHERAL=y -# CONFIG_ESP_TLS_CLIENT_SESSION_TICKETS is not set -# CONFIG_ESP_TLS_SERVER_SESSION_TICKETS is not set -# CONFIG_ESP_TLS_SERVER_CERT_SELECT_HOOK is not set -# CONFIG_ESP_TLS_SERVER_MIN_AUTH_MODE_OPTIONAL is not set -# CONFIG_ESP_TLS_PSK_VERIFICATION is not set -# CONFIG_ESP_TLS_INSECURE is not set -# end of ESP-TLS - -# -# ADC and ADC Calibration -# -# CONFIG_ADC_ONESHOT_CTRL_FUNC_IN_IRAM is not set -# CONFIG_ADC_CONTINUOUS_ISR_IRAM_SAFE is not set -# CONFIG_ADC_CONTINUOUS_FORCE_USE_ADC2_ON_C3_S3 is not set -# CONFIG_ADC_ONESHOT_FORCE_USE_ADC2_ON_C3 is not set -# CONFIG_ADC_ENABLE_DEBUG_LOG is not set -# end of ADC and ADC Calibration - -# -# Wireless Coexistence -# -CONFIG_ESP_COEX_ENABLED=y -# CONFIG_ESP_COEX_EXTERNAL_COEXIST_ENABLE is not set -# CONFIG_ESP_COEX_GPIO_DEBUG is not set -# end of Wireless Coexistence - # # Common ESP-related # @@ -784,81 +733,6 @@ CONFIG_SPI_SLAVE_ISR_IN_IRAM=y CONFIG_USJ_ENABLE_USB_SERIAL_JTAG=y # end of ESP-Driver:USB Serial/JTAG Configuration -# -# Ethernet -# -CONFIG_ETH_ENABLED=y -CONFIG_ETH_USE_SPI_ETHERNET=y -# CONFIG_ETH_SPI_ETHERNET_DM9051 is not set -# CONFIG_ETH_SPI_ETHERNET_W5500 is not set -# CONFIG_ETH_SPI_ETHERNET_KSZ8851SNL is not set -# CONFIG_ETH_USE_OPENETH is not set -# CONFIG_ETH_TRANSMIT_MUTEX is not set -# end of Ethernet - -# -# Event Loop Library -# -# CONFIG_ESP_EVENT_LOOP_PROFILING is not set -CONFIG_ESP_EVENT_POST_FROM_ISR=y -CONFIG_ESP_EVENT_POST_FROM_IRAM_ISR=y -# end of Event Loop Library - -# -# GDB Stub -# -CONFIG_ESP_GDBSTUB_ENABLED=y -# CONFIG_ESP_SYSTEM_GDBSTUB_RUNTIME is not set -CONFIG_ESP_GDBSTUB_SUPPORT_TASKS=y -CONFIG_ESP_GDBSTUB_MAX_TASKS=32 -# end of GDB Stub - -# -# ESP HID -# -CONFIG_ESPHID_TASK_SIZE_BT=2048 -CONFIG_ESPHID_TASK_SIZE_BLE=4096 -# end of ESP HID - -# -# ESP HTTP client -# -CONFIG_ESP_HTTP_CLIENT_ENABLE_HTTPS=y -# CONFIG_ESP_HTTP_CLIENT_ENABLE_BASIC_AUTH is not set -# CONFIG_ESP_HTTP_CLIENT_ENABLE_DIGEST_AUTH is not set -# CONFIG_ESP_HTTP_CLIENT_ENABLE_CUSTOM_TRANSPORT is not set -CONFIG_ESP_HTTP_CLIENT_EVENT_POST_TIMEOUT=2000 -# end of ESP HTTP client - -# -# HTTP Server -# -CONFIG_HTTPD_MAX_REQ_HDR_LEN=1024 -CONFIG_HTTPD_MAX_URI_LEN=512 -CONFIG_HTTPD_ERR_RESP_NO_DELAY=y -CONFIG_HTTPD_PURGE_BUF_LEN=32 -# CONFIG_HTTPD_LOG_PURGE_DATA is not set -# CONFIG_HTTPD_WS_SUPPORT is not set -# CONFIG_HTTPD_QUEUE_WORK_BLOCKING is not set -CONFIG_HTTPD_SERVER_EVENT_POST_TIMEOUT=2000 -# end of HTTP Server - -# -# ESP HTTPS OTA -# -# CONFIG_ESP_HTTPS_OTA_DECRYPT_CB is not set -# CONFIG_ESP_HTTPS_OTA_ALLOW_HTTP is not set -CONFIG_ESP_HTTPS_OTA_EVENT_POST_TIMEOUT=2000 -# end of ESP HTTPS OTA - -# -# ESP HTTPS server -# -# CONFIG_ESP_HTTPS_SERVER_ENABLE is not set -CONFIG_ESP_HTTPS_SERVER_EVENT_POST_TIMEOUT=2000 -# CONFIG_ESP_HTTPS_SERVER_CERT_SELECT_HOOK is not set -# end of ESP HTTPS server - # # Hardware Settings # @@ -984,46 +858,11 @@ CONFIG_ESP_INTR_IN_IRAM=y # # end of ESP-MM: Memory Management Configurations -# -# ESP NETIF Adapter -# -CONFIG_ESP_NETIF_IP_LOST_TIMER_INTERVAL=120 -# CONFIG_ESP_NETIF_PROVIDE_CUSTOM_IMPLEMENTATION is not set -CONFIG_ESP_NETIF_TCPIP_LWIP=y -# CONFIG_ESP_NETIF_LOOPBACK is not set -CONFIG_ESP_NETIF_USES_TCPIP_WITH_BSD_API=y -CONFIG_ESP_NETIF_REPORT_DATA_TRAFFIC=y -# CONFIG_ESP_NETIF_RECEIVE_REPORT_ERRORS is not set -# CONFIG_ESP_NETIF_L2_TAP is not set -# CONFIG_ESP_NETIF_BRIDGE_EN is not set -# CONFIG_ESP_NETIF_SET_DNS_PER_DEFAULT_NETIF is not set -# end of ESP NETIF Adapter - # # Partition API Configuration # # end of Partition API Configuration -# -# PHY -# -CONFIG_ESP_PHY_ENABLED=y -CONFIG_ESP_PHY_CALIBRATION_AND_DATA_STORAGE=y -# CONFIG_ESP_PHY_INIT_DATA_IN_PARTITION is not set -CONFIG_ESP_PHY_MAX_WIFI_TX_POWER=20 -CONFIG_ESP_PHY_MAX_TX_POWER=20 -# CONFIG_ESP_PHY_REDUCE_TX_POWER is not set -CONFIG_ESP_PHY_ENABLE_USB=y -# CONFIG_ESP_PHY_ENABLE_CERT_TEST is not set -CONFIG_ESP_PHY_RF_CAL_PARTIAL=y -# CONFIG_ESP_PHY_RF_CAL_NONE is not set -# CONFIG_ESP_PHY_RF_CAL_FULL is not set -CONFIG_ESP_PHY_CALIBRATION_MODE=0 -# CONFIG_ESP_PHY_PLL_TRACK_DEBUG is not set -# CONFIG_ESP_PHY_RECORD_USED_TIME is not set -CONFIG_ESP_PHY_IRAM_OPT=y -# end of PHY - # # Power Management # @@ -1063,7 +902,6 @@ CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ=160 # CONFIG_ESP_SYSTEM_PANIC_PRINT_HALT is not set CONFIG_ESP_SYSTEM_PANIC_PRINT_REBOOT=y # CONFIG_ESP_SYSTEM_PANIC_SILENT_REBOOT is not set -# CONFIG_ESP_SYSTEM_PANIC_GDBSTUB is not set CONFIG_ESP_SYSTEM_PANIC_REBOOT_DELAY_SECONDS=0 CONFIG_ESP_SYSTEM_SINGLE_CORE_MODE=y CONFIG_ESP_SYSTEM_RTC_FAST_MEM_AS_HEAP_DEPCHECK=y @@ -1132,132 +970,6 @@ CONFIG_ESP_TIMER_ISR_AFFINITY_CPU0=y CONFIG_ESP_TIMER_IMPL_SYSTIMER=y # end of ESP Timer (High Resolution Timer) -# -# Wi-Fi -# -CONFIG_ESP_WIFI_ENABLED=y -CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM=10 -CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM=32 -# CONFIG_ESP_WIFI_STATIC_TX_BUFFER is not set -CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER=y -CONFIG_ESP_WIFI_TX_BUFFER_TYPE=1 -CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER_NUM=32 -CONFIG_ESP_WIFI_STATIC_RX_MGMT_BUFFER=y -# CONFIG_ESP_WIFI_DYNAMIC_RX_MGMT_BUFFER is not set -CONFIG_ESP_WIFI_DYNAMIC_RX_MGMT_BUF=0 -CONFIG_ESP_WIFI_RX_MGMT_BUF_NUM_DEF=5 -# CONFIG_ESP_WIFI_CSI_ENABLED is not set -CONFIG_ESP_WIFI_AMPDU_TX_ENABLED=y -CONFIG_ESP_WIFI_TX_BA_WIN=6 -CONFIG_ESP_WIFI_AMPDU_RX_ENABLED=y -CONFIG_ESP_WIFI_RX_BA_WIN=6 -CONFIG_ESP_WIFI_NVS_ENABLED=y -CONFIG_ESP_WIFI_SOFTAP_BEACON_MAX_LEN=752 -CONFIG_ESP_WIFI_MGMT_SBUF_NUM=32 -CONFIG_ESP_WIFI_IRAM_OPT=y -# CONFIG_ESP_WIFI_EXTRA_IRAM_OPT is not set -CONFIG_ESP_WIFI_RX_IRAM_OPT=y -CONFIG_ESP_WIFI_ENABLE_WPA3_SAE=y -CONFIG_ESP_WIFI_ENABLE_SAE_PK=y -CONFIG_ESP_WIFI_ENABLE_SAE_H2E=y -CONFIG_ESP_WIFI_SOFTAP_SAE_SUPPORT=y -CONFIG_ESP_WIFI_ENABLE_WPA3_OWE_STA=y -# CONFIG_ESP_WIFI_SLP_IRAM_OPT is not set -CONFIG_ESP_WIFI_SLP_DEFAULT_MIN_ACTIVE_TIME=50 -# CONFIG_ESP_WIFI_BSS_MAX_IDLE_SUPPORT is not set -CONFIG_ESP_WIFI_SLP_DEFAULT_MAX_ACTIVE_TIME=10 -CONFIG_ESP_WIFI_SLP_DEFAULT_WAIT_BROADCAST_DATA_TIME=15 -# CONFIG_ESP_WIFI_FTM_ENABLE is not set -CONFIG_ESP_WIFI_STA_DISCONNECTED_PM_ENABLE=y -# CONFIG_ESP_WIFI_GCMP_SUPPORT is not set -CONFIG_ESP_WIFI_GMAC_SUPPORT=y -CONFIG_ESP_WIFI_SOFTAP_SUPPORT=y -# CONFIG_ESP_WIFI_SLP_BEACON_LOST_OPT is not set -CONFIG_ESP_WIFI_ESPNOW_MAX_ENCRYPT_NUM=7 -CONFIG_ESP_WIFI_MBEDTLS_CRYPTO=y -CONFIG_ESP_WIFI_MBEDTLS_TLS_CLIENT=y -# CONFIG_ESP_WIFI_WAPI_PSK is not set -# CONFIG_ESP_WIFI_SUITE_B_192 is not set -# CONFIG_ESP_WIFI_11KV_SUPPORT is not set -# CONFIG_ESP_WIFI_MBO_SUPPORT is not set -# CONFIG_ESP_WIFI_DPP_SUPPORT is not set -# CONFIG_ESP_WIFI_11R_SUPPORT is not set -# CONFIG_ESP_WIFI_WPS_SOFTAP_REGISTRAR is not set - -# -# WPS Configuration Options -# -# CONFIG_ESP_WIFI_WPS_STRICT is not set -# CONFIG_ESP_WIFI_WPS_PASSPHRASE is not set -# end of WPS Configuration Options - -# CONFIG_ESP_WIFI_DEBUG_PRINT is not set -# CONFIG_ESP_WIFI_TESTING_OPTIONS is not set -CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT=y -# CONFIG_ESP_WIFI_ENT_FREE_DYNAMIC_BUFFER is not set -# end of Wi-Fi - -# -# Core dump -# -# CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH is not set -# CONFIG_ESP_COREDUMP_ENABLE_TO_UART is not set -CONFIG_ESP_COREDUMP_ENABLE_TO_NONE=y -# end of Core dump - -# -# FAT Filesystem support -# -CONFIG_FATFS_VOLUME_COUNT=2 -CONFIG_FATFS_LFN_NONE=y -# CONFIG_FATFS_LFN_HEAP is not set -# CONFIG_FATFS_LFN_STACK is not set -# CONFIG_FATFS_SECTOR_512 is not set -CONFIG_FATFS_SECTOR_4096=y -# CONFIG_FATFS_CODEPAGE_DYNAMIC is not set -CONFIG_FATFS_CODEPAGE_437=y -# CONFIG_FATFS_CODEPAGE_720 is not set -# CONFIG_FATFS_CODEPAGE_737 is not set -# CONFIG_FATFS_CODEPAGE_771 is not set -# CONFIG_FATFS_CODEPAGE_775 is not set -# CONFIG_FATFS_CODEPAGE_850 is not set -# CONFIG_FATFS_CODEPAGE_852 is not set -# CONFIG_FATFS_CODEPAGE_855 is not set -# CONFIG_FATFS_CODEPAGE_857 is not set -# CONFIG_FATFS_CODEPAGE_860 is not set -# CONFIG_FATFS_CODEPAGE_861 is not set -# CONFIG_FATFS_CODEPAGE_862 is not set -# CONFIG_FATFS_CODEPAGE_863 is not set -# CONFIG_FATFS_CODEPAGE_864 is not set -# CONFIG_FATFS_CODEPAGE_865 is not set -# CONFIG_FATFS_CODEPAGE_866 is not set -# CONFIG_FATFS_CODEPAGE_869 is not set -# CONFIG_FATFS_CODEPAGE_932 is not set -# CONFIG_FATFS_CODEPAGE_936 is not set -# CONFIG_FATFS_CODEPAGE_949 is not set -# CONFIG_FATFS_CODEPAGE_950 is not set -CONFIG_FATFS_CODEPAGE=437 -CONFIG_FATFS_FS_LOCK=0 -CONFIG_FATFS_TIMEOUT_MS=10000 -CONFIG_FATFS_PER_FILE_CACHE=y -# CONFIG_FATFS_USE_FASTSEEK is not set -CONFIG_FATFS_USE_STRFUNC_NONE=y -# CONFIG_FATFS_USE_STRFUNC_WITHOUT_CRLF_CONV is not set -# CONFIG_FATFS_USE_STRFUNC_WITH_CRLF_CONV is not set -CONFIG_FATFS_VFS_FSTAT_BLKSIZE=0 -# CONFIG_FATFS_IMMEDIATE_FSYNC is not set -# CONFIG_FATFS_USE_LABEL is not set -CONFIG_FATFS_LINK_LOCK=y -# CONFIG_FATFS_USE_DYN_BUFFERS is not set - -# -# File system free space calculation behavior -# -CONFIG_FATFS_DONT_TRUST_FREE_CLUSTER_CNT=0 -CONFIG_FATFS_DONT_TRUST_LAST_ALLOC=0 -# end of File system free space calculation behavior -# end of FAT Filesystem support - # # FreeRTOS # @@ -1412,192 +1124,6 @@ CONFIG_LOG_MODE_TEXT=y CONFIG_LOG_IN_IRAM=y # end of Log -# -# LWIP -# -CONFIG_LWIP_ENABLE=y -CONFIG_LWIP_LOCAL_HOSTNAME="espressif" -CONFIG_LWIP_TCPIP_TASK_PRIO=18 -# CONFIG_LWIP_TCPIP_CORE_LOCKING is not set -# CONFIG_LWIP_CHECK_THREAD_SAFETY is not set -CONFIG_LWIP_DNS_SUPPORT_MDNS_QUERIES=y -# CONFIG_LWIP_L2_TO_L3_COPY is not set -# CONFIG_LWIP_IRAM_OPTIMIZATION is not set -# CONFIG_LWIP_EXTRA_IRAM_OPTIMIZATION is not set -CONFIG_LWIP_TIMERS_ONDEMAND=y -CONFIG_LWIP_ND6=y -# CONFIG_LWIP_FORCE_ROUTER_FORWARDING is not set -CONFIG_LWIP_MAX_SOCKETS=10 -# CONFIG_LWIP_USE_ONLY_LWIP_SELECT is not set -# CONFIG_LWIP_SO_LINGER is not set -CONFIG_LWIP_SO_REUSE=y -CONFIG_LWIP_SO_REUSE_RXTOALL=y -# CONFIG_LWIP_SO_RCVBUF is not set -# CONFIG_LWIP_NETBUF_RECVINFO is not set -CONFIG_LWIP_IP_DEFAULT_TTL=64 -CONFIG_LWIP_IP4_FRAG=y -CONFIG_LWIP_IP6_FRAG=y -# CONFIG_LWIP_IP4_REASSEMBLY is not set -# CONFIG_LWIP_IP6_REASSEMBLY is not set -CONFIG_LWIP_IP_REASS_MAX_PBUFS=10 -# CONFIG_LWIP_IP_FORWARD is not set -# CONFIG_LWIP_STATS is not set -CONFIG_LWIP_ESP_GRATUITOUS_ARP=y -CONFIG_LWIP_GARP_TMR_INTERVAL=60 -CONFIG_LWIP_ESP_MLDV6_REPORT=y -CONFIG_LWIP_MLDV6_TMR_INTERVAL=40 -CONFIG_LWIP_TCPIP_RECVMBOX_SIZE=32 -CONFIG_LWIP_DHCP_DOES_ARP_CHECK=y -# CONFIG_LWIP_DHCP_DOES_ACD_CHECK is not set -# CONFIG_LWIP_DHCP_DOES_NOT_CHECK_OFFERED_IP is not set -# CONFIG_LWIP_DHCP_DISABLE_CLIENT_ID is not set -CONFIG_LWIP_DHCP_DISABLE_VENDOR_CLASS_ID=y -# CONFIG_LWIP_DHCP_RESTORE_LAST_IP is not set -CONFIG_LWIP_DHCP_OPTIONS_LEN=68 -CONFIG_LWIP_NUM_NETIF_CLIENT_DATA=0 -CONFIG_LWIP_DHCP_COARSE_TIMER_SECS=1 - -# -# DHCP server -# -CONFIG_LWIP_DHCPS=y -CONFIG_LWIP_DHCPS_LEASE_UNIT=60 -CONFIG_LWIP_DHCPS_MAX_STATION_NUM=8 -CONFIG_LWIP_DHCPS_STATIC_ENTRIES=y -CONFIG_LWIP_DHCPS_ADD_DNS=y -# end of DHCP server - -# CONFIG_LWIP_AUTOIP is not set -CONFIG_LWIP_IPV4=y -CONFIG_LWIP_IPV6=y -# CONFIG_LWIP_IPV6_AUTOCONFIG is not set -CONFIG_LWIP_IPV6_NUM_ADDRESSES=3 -# CONFIG_LWIP_IPV6_FORWARD is not set -# CONFIG_LWIP_NETIF_STATUS_CALLBACK is not set -CONFIG_LWIP_NETIF_LOOPBACK=y -CONFIG_LWIP_LOOPBACK_MAX_PBUFS=8 - -# -# TCP -# -CONFIG_LWIP_MAX_ACTIVE_TCP=16 -CONFIG_LWIP_MAX_LISTENING_TCP=16 -CONFIG_LWIP_TCP_HIGH_SPEED_RETRANSMISSION=y -CONFIG_LWIP_TCP_MAXRTX=12 -CONFIG_LWIP_TCP_SYNMAXRTX=12 -CONFIG_LWIP_TCP_MSS=1440 -CONFIG_LWIP_TCP_TMR_INTERVAL=250 -CONFIG_LWIP_TCP_MSL=60000 -CONFIG_LWIP_TCP_FIN_WAIT_TIMEOUT=20000 -CONFIG_LWIP_TCP_SND_BUF_DEFAULT=5760 -CONFIG_LWIP_TCP_WND_DEFAULT=5760 -CONFIG_LWIP_TCP_RECVMBOX_SIZE=6 -CONFIG_LWIP_TCP_ACCEPTMBOX_SIZE=6 -CONFIG_LWIP_TCP_QUEUE_OOSEQ=y -CONFIG_LWIP_TCP_OOSEQ_TIMEOUT=6 -CONFIG_LWIP_TCP_OOSEQ_MAX_PBUFS=4 -# CONFIG_LWIP_TCP_SACK_OUT is not set -CONFIG_LWIP_TCP_OVERSIZE_MSS=y -# CONFIG_LWIP_TCP_OVERSIZE_QUARTER_MSS is not set -# CONFIG_LWIP_TCP_OVERSIZE_DISABLE is not set -CONFIG_LWIP_TCP_RTO_TIME=1500 -# end of TCP - -# -# UDP -# -CONFIG_LWIP_MAX_UDP_PCBS=16 -CONFIG_LWIP_UDP_RECVMBOX_SIZE=6 -# end of UDP - -# -# Checksums -# -# CONFIG_LWIP_CHECKSUM_CHECK_IP is not set -# CONFIG_LWIP_CHECKSUM_CHECK_UDP is not set -CONFIG_LWIP_CHECKSUM_CHECK_ICMP=y -# end of Checksums - -CONFIG_LWIP_TCPIP_TASK_STACK_SIZE=3072 -CONFIG_LWIP_TCPIP_TASK_AFFINITY_NO_AFFINITY=y -# CONFIG_LWIP_TCPIP_TASK_AFFINITY_CPU0 is not set -CONFIG_LWIP_TCPIP_TASK_AFFINITY=0x7FFFFFFF -CONFIG_LWIP_IPV6_MEMP_NUM_ND6_QUEUE=3 -CONFIG_LWIP_IPV6_ND6_NUM_NEIGHBORS=5 -CONFIG_LWIP_IPV6_ND6_NUM_PREFIXES=5 -CONFIG_LWIP_IPV6_ND6_NUM_ROUTERS=3 -CONFIG_LWIP_IPV6_ND6_NUM_DESTINATIONS=10 -# CONFIG_LWIP_PPP_SUPPORT is not set -# CONFIG_LWIP_SLIP_SUPPORT is not set - -# -# ICMP -# -CONFIG_LWIP_ICMP=y -# CONFIG_LWIP_MULTICAST_PING is not set -# CONFIG_LWIP_BROADCAST_PING is not set -# end of ICMP - -# -# LWIP RAW API -# -CONFIG_LWIP_MAX_RAW_PCBS=16 -# end of LWIP RAW API - -# -# SNTP -# -CONFIG_LWIP_SNTP_MAX_SERVERS=1 -# CONFIG_LWIP_DHCP_GET_NTP_SRV is not set -CONFIG_LWIP_SNTP_UPDATE_DELAY=3600000 -CONFIG_LWIP_SNTP_STARTUP_DELAY=y -CONFIG_LWIP_SNTP_MAXIMUM_STARTUP_DELAY=5000 -# end of SNTP - -# -# DNS -# -CONFIG_LWIP_DNS_MAX_HOST_IP=1 -CONFIG_LWIP_DNS_MAX_SERVERS=3 -# CONFIG_LWIP_FALLBACK_DNS_SERVER_SUPPORT is not set -# CONFIG_LWIP_DNS_SETSERVER_WITH_NETIF is not set -# CONFIG_LWIP_USE_ESP_GETADDRINFO is not set -# end of DNS - -CONFIG_LWIP_BRIDGEIF_MAX_PORTS=7 -CONFIG_LWIP_ESP_LWIP_ASSERT=y - -# -# Hooks -# -# CONFIG_LWIP_HOOK_TCP_ISN_NONE is not set -CONFIG_LWIP_HOOK_TCP_ISN_DEFAULT=y -# CONFIG_LWIP_HOOK_TCP_ISN_CUSTOM is not set -CONFIG_LWIP_HOOK_IP6_ROUTE_NONE=y -# CONFIG_LWIP_HOOK_IP6_ROUTE_DEFAULT is not set -# CONFIG_LWIP_HOOK_IP6_ROUTE_CUSTOM is not set -CONFIG_LWIP_HOOK_ND6_GET_GW_NONE=y -# CONFIG_LWIP_HOOK_ND6_GET_GW_DEFAULT is not set -# CONFIG_LWIP_HOOK_ND6_GET_GW_CUSTOM is not set -CONFIG_LWIP_HOOK_IP6_SELECT_SRC_ADDR_NONE=y -# CONFIG_LWIP_HOOK_IP6_SELECT_SRC_ADDR_DEFAULT is not set -# CONFIG_LWIP_HOOK_IP6_SELECT_SRC_ADDR_CUSTOM is not set -CONFIG_LWIP_HOOK_DHCP_EXTRA_OPTION_NONE=y -# CONFIG_LWIP_HOOK_DHCP_EXTRA_OPTION_DEFAULT is not set -# CONFIG_LWIP_HOOK_DHCP_EXTRA_OPTION_CUSTOM is not set -CONFIG_LWIP_HOOK_NETCONN_EXT_RESOLVE_NONE=y -# CONFIG_LWIP_HOOK_NETCONN_EXT_RESOLVE_DEFAULT is not set -# CONFIG_LWIP_HOOK_NETCONN_EXT_RESOLVE_CUSTOM is not set -CONFIG_LWIP_HOOK_DNS_EXT_RESOLVE_NONE=y -# CONFIG_LWIP_HOOK_DNS_EXT_RESOLVE_CUSTOM is not set -# CONFIG_LWIP_HOOK_IP6_INPUT_NONE is not set -CONFIG_LWIP_HOOK_IP6_INPUT_DEFAULT=y -# CONFIG_LWIP_HOOK_IP6_INPUT_CUSTOM is not set -# end of Hooks - -# CONFIG_LWIP_DEBUG is not set -# end of LWIP - # # mbedTLS # @@ -1732,38 +1258,15 @@ CONFIG_MBEDTLS_ECP_NIST_OPTIM=y # CONFIG_MBEDTLS_HKDF_C is not set # CONFIG_MBEDTLS_THREADING_C is not set CONFIG_MBEDTLS_ERROR_STRINGS=y -CONFIG_MBEDTLS_FS_IO=y # CONFIG_MBEDTLS_ALLOW_WEAK_CERTIFICATE_VERIFICATION is not set # end of mbedTLS -# -# ESP-MQTT Configurations -# -CONFIG_MQTT_PROTOCOL_311=y -# CONFIG_MQTT_PROTOCOL_5 is not set -CONFIG_MQTT_TRANSPORT_SSL=y -CONFIG_MQTT_TRANSPORT_WEBSOCKET=y -CONFIG_MQTT_TRANSPORT_WEBSOCKET_SECURE=y -# CONFIG_MQTT_MSG_ID_INCREMENTAL is not set -# CONFIG_MQTT_SKIP_PUBLISH_IF_DISCONNECTED is not set -# CONFIG_MQTT_REPORT_DELETED_MESSAGES is not set -# CONFIG_MQTT_USE_CUSTOM_CONFIG is not set -# CONFIG_MQTT_TASK_CORE_SELECTION_ENABLED is not set -# CONFIG_MQTT_CUSTOM_OUTBOX is not set -# end of ESP-MQTT Configurations - # # LibC # CONFIG_LIBC_NEWLIB=y CONFIG_LIBC_MISC_IN_IRAM=y CONFIG_LIBC_LOCKS_PLACE_IN_IRAM=y -CONFIG_LIBC_STDOUT_LINE_ENDING_CRLF=y -# CONFIG_LIBC_STDOUT_LINE_ENDING_LF is not set -# CONFIG_LIBC_STDOUT_LINE_ENDING_CR is not set -# CONFIG_LIBC_STDIN_LINE_ENDING_CRLF is not set -# CONFIG_LIBC_STDIN_LINE_ENDING_LF is not set -CONFIG_LIBC_STDIN_LINE_ENDING_CR=y # CONFIG_LIBC_NEWLIB_NANO_FORMAT is not set CONFIG_LIBC_TIME_SYSCALL_USE_RTC_HRT=y # CONFIG_LIBC_TIME_SYSCALL_USE_RTC is not set @@ -1772,35 +1275,6 @@ CONFIG_LIBC_TIME_SYSCALL_USE_RTC_HRT=y # CONFIG_LIBC_OPTIMIZED_MISALIGNED_ACCESS is not set # end of LibC -# -# NVS -# -# CONFIG_NVS_ENCRYPTION is not set -# CONFIG_NVS_ASSERT_ERROR_CHECK is not set -# CONFIG_NVS_LEGACY_DUP_KEYS_COMPATIBILITY is not set -# end of NVS - -# -# OpenThread -# -# CONFIG_OPENTHREAD_ENABLED is not set - -# -# OpenThread Spinel -# -# CONFIG_OPENTHREAD_SPINEL_ONLY is not set -# end of OpenThread Spinel -# end of OpenThread - -# -# Protocomm -# -CONFIG_ESP_PROTOCOMM_SUPPORT_SECURITY_VERSION_0=y -CONFIG_ESP_PROTOCOMM_SUPPORT_SECURITY_VERSION_1=y -CONFIG_ESP_PROTOCOMM_SUPPORT_SECURITY_VERSION_2=y -CONFIG_ESP_PROTOCOMM_SUPPORT_SECURITY_PATCH_VERSION=y -# end of Protocomm - # # PThreads # @@ -1885,105 +1359,6 @@ CONFIG_SPI_FLASH_SUPPORT_TH_CHIP=y CONFIG_SPI_FLASH_ENABLE_ENCRYPTED_READ_WRITE=y # end of SPI Flash driver -# -# SPIFFS Configuration -# -CONFIG_SPIFFS_MAX_PARTITIONS=3 - -# -# SPIFFS Cache Configuration -# -CONFIG_SPIFFS_CACHE=y -CONFIG_SPIFFS_CACHE_WR=y -# CONFIG_SPIFFS_CACHE_STATS is not set -# end of SPIFFS Cache Configuration - -CONFIG_SPIFFS_PAGE_CHECK=y -CONFIG_SPIFFS_GC_MAX_RUNS=10 -# CONFIG_SPIFFS_GC_STATS is not set -CONFIG_SPIFFS_PAGE_SIZE=256 -CONFIG_SPIFFS_OBJ_NAME_LEN=32 -# CONFIG_SPIFFS_FOLLOW_SYMLINKS is not set -CONFIG_SPIFFS_USE_MAGIC=y -CONFIG_SPIFFS_USE_MAGIC_LENGTH=y -CONFIG_SPIFFS_META_LENGTH=4 -CONFIG_SPIFFS_USE_MTIME=y - -# -# Debug Configuration -# -# CONFIG_SPIFFS_DBG is not set -# CONFIG_SPIFFS_API_DBG is not set -# CONFIG_SPIFFS_GC_DBG is not set -# CONFIG_SPIFFS_CACHE_DBG is not set -# CONFIG_SPIFFS_CHECK_DBG is not set -# CONFIG_SPIFFS_TEST_VISUALISATION is not set -# end of Debug Configuration -# end of SPIFFS Configuration - -# -# TCP Transport -# - -# -# Websocket -# -CONFIG_WS_TRANSPORT=y -CONFIG_WS_BUFFER_SIZE=1024 -# CONFIG_WS_DYNAMIC_BUFFER is not set -# end of Websocket -# end of TCP Transport - -# -# Unity unit testing library -# -CONFIG_UNITY_ENABLE_FLOAT=y -CONFIG_UNITY_ENABLE_DOUBLE=y -# CONFIG_UNITY_ENABLE_64BIT is not set -# CONFIG_UNITY_ENABLE_COLOR is not set -CONFIG_UNITY_ENABLE_IDF_TEST_RUNNER=y -# CONFIG_UNITY_ENABLE_FIXTURE is not set -# CONFIG_UNITY_ENABLE_BACKTRACE_ON_FAIL is not set -# CONFIG_UNITY_TEST_ORDER_BY_FILE_PATH_AND_LINE is not set -# end of Unity unit testing library - -# -# Virtual file system -# -CONFIG_VFS_SUPPORT_IO=y -CONFIG_VFS_SUPPORT_DIR=y -CONFIG_VFS_SUPPORT_SELECT=y -CONFIG_VFS_SUPPRESS_SELECT_DEBUG_OUTPUT=y -# CONFIG_VFS_SELECT_IN_RAM is not set -CONFIG_VFS_SUPPORT_TERMIOS=y -CONFIG_VFS_MAX_COUNT=8 - -# -# Host File System I/O (Semihosting) -# -CONFIG_VFS_SEMIHOSTFS_MAX_MOUNT_POINTS=1 -# end of Host File System I/O (Semihosting) - -CONFIG_VFS_INITIALIZE_DEV_NULL=y -# end of Virtual file system - -# -# Wear Levelling -# -# CONFIG_WL_SECTOR_SIZE_512 is not set -CONFIG_WL_SECTOR_SIZE_4096=y -CONFIG_WL_SECTOR_SIZE=4096 -# end of Wear Levelling - -# -# Wi-Fi Provisioning Manager -# -CONFIG_WIFI_PROV_SCAN_MAX_ENTRIES=16 -CONFIG_WIFI_PROV_AUTOSTOP_TIMEOUT=30 -CONFIG_WIFI_PROV_STA_ALL_CHANNEL_SCAN=y -# CONFIG_WIFI_PROV_STA_FAST_SCAN is not set -# end of Wi-Fi Provisioning Manager - # # INA226 # @@ -2067,18 +1442,7 @@ CONFIG_STACK_CHECK_NONE=y # CONFIG_STACK_CHECK_STRONG is not set # CONFIG_STACK_CHECK_ALL is not set # CONFIG_WARN_WRITE_STRINGS is not set -# CONFIG_ESP32_APPTRACE_DEST_TRAX is not set -CONFIG_ESP32_APPTRACE_DEST_NONE=y -CONFIG_ESP32_APPTRACE_LOCK_ENABLE=y -# CONFIG_EXTERNAL_COEX_ENABLE is not set -# CONFIG_ESP_WIFI_EXTERNAL_COEXIST_ENABLE is not set # CONFIG_GPTIMER_ISR_IRAM_SAFE is not set -# CONFIG_EVENT_LOOP_PROFILING is not set -CONFIG_POST_EVENTS_FROM_ISR=y -CONFIG_POST_EVENTS_FROM_IRAM_ISR=y -CONFIG_GDBSTUB_SUPPORT_TASKS=y -CONFIG_GDBSTUB_MAX_TASKS=32 -# CONFIG_OTA_ALLOW_HTTP is not set # CONFIG_ESP_SYSTEM_PD_FLASH is not set CONFIG_ESP32C3_LIGHTSLEEP_GPIO_RESET_WORKAROUND=y CONFIG_ESP32C3_RTC_CLK_SRC_INT_RC=y @@ -2104,12 +1468,6 @@ CONFIG_ESP32C3_BROWNOUT_DET_LVL_SEL_7=y CONFIG_BROWNOUT_DET_LVL=7 CONFIG_ESP32C3_BROWNOUT_DET_LVL=7 CONFIG_ESP_SYSTEM_BROWNOUT_INTR=y -CONFIG_ESP32_PHY_CALIBRATION_AND_DATA_STORAGE=y -# CONFIG_ESP32_PHY_INIT_DATA_IN_PARTITION is not set -CONFIG_ESP32_PHY_MAX_WIFI_TX_POWER=20 -CONFIG_ESP32_PHY_MAX_TX_POWER=20 -# CONFIG_REDUCE_PHY_TX_POWER is not set -# CONFIG_ESP32_REDUCE_PHY_TX_POWER is not set CONFIG_ESP_SYSTEM_PM_POWER_DOWN_CPU=y # CONFIG_ESP32C3_DEFAULT_CPU_FREQ_80 is not set CONFIG_ESP32C3_DEFAULT_CPU_FREQ_160=y @@ -2135,72 +1493,11 @@ CONFIG_TASK_WDT_CHECK_IDLE_TASK_CPU0=y CONFIG_ESP32C3_DEBUG_OCDAWARE=y CONFIG_IPC_TASK_STACK_SIZE=1024 CONFIG_TIMER_TASK_STACK_SIZE=3584 -CONFIG_ESP32_WIFI_ENABLED=y -CONFIG_ESP32_WIFI_STATIC_RX_BUFFER_NUM=10 -CONFIG_ESP32_WIFI_DYNAMIC_RX_BUFFER_NUM=32 -# CONFIG_ESP32_WIFI_STATIC_TX_BUFFER is not set -CONFIG_ESP32_WIFI_DYNAMIC_TX_BUFFER=y -CONFIG_ESP32_WIFI_TX_BUFFER_TYPE=1 -CONFIG_ESP32_WIFI_DYNAMIC_TX_BUFFER_NUM=32 -# CONFIG_ESP32_WIFI_CSI_ENABLED is not set -CONFIG_ESP32_WIFI_AMPDU_TX_ENABLED=y -CONFIG_ESP32_WIFI_TX_BA_WIN=6 -CONFIG_ESP32_WIFI_AMPDU_RX_ENABLED=y -CONFIG_ESP32_WIFI_RX_BA_WIN=6 -CONFIG_ESP32_WIFI_NVS_ENABLED=y -CONFIG_ESP32_WIFI_SOFTAP_BEACON_MAX_LEN=752 -CONFIG_ESP32_WIFI_MGMT_SBUF_NUM=32 -CONFIG_ESP32_WIFI_IRAM_OPT=y -CONFIG_ESP32_WIFI_RX_IRAM_OPT=y -CONFIG_ESP32_WIFI_ENABLE_WPA3_SAE=y -CONFIG_ESP32_WIFI_ENABLE_WPA3_OWE_STA=y -CONFIG_WPA_MBEDTLS_CRYPTO=y -CONFIG_WPA_MBEDTLS_TLS_CLIENT=y -# CONFIG_WPA_WAPI_PSK is not set -# CONFIG_WPA_SUITE_B_192 is not set -# CONFIG_WPA_11KV_SUPPORT is not set -# CONFIG_WPA_MBO_SUPPORT is not set -# CONFIG_WPA_DPP_SUPPORT is not set -# CONFIG_WPA_11R_SUPPORT is not set -# CONFIG_WPA_WPS_SOFTAP_REGISTRAR is not set -# CONFIG_WPA_WPS_STRICT is not set -# CONFIG_WPA_DEBUG_PRINT is not set -# CONFIG_WPA_TESTING_OPTIONS is not set -# CONFIG_ESP32_ENABLE_COREDUMP_TO_FLASH is not set -# CONFIG_ESP32_ENABLE_COREDUMP_TO_UART is not set -CONFIG_ESP32_ENABLE_COREDUMP_TO_NONE=y CONFIG_TIMER_TASK_PRIORITY=1 CONFIG_TIMER_TASK_STACK_DEPTH=2048 CONFIG_TIMER_QUEUE_LENGTH=10 # CONFIG_ENABLE_STATIC_TASK_CLEAN_UP_HOOK is not set # CONFIG_HAL_ASSERTION_SILIENT is not set -# CONFIG_L2_TO_L3_COPY is not set -CONFIG_ESP_GRATUITOUS_ARP=y -CONFIG_GARP_TMR_INTERVAL=60 -CONFIG_TCPIP_RECVMBOX_SIZE=32 -CONFIG_TCP_MAXRTX=12 -CONFIG_TCP_SYNMAXRTX=12 -CONFIG_TCP_MSS=1440 -CONFIG_TCP_MSL=60000 -CONFIG_TCP_SND_BUF_DEFAULT=5760 -CONFIG_TCP_WND_DEFAULT=5760 -CONFIG_TCP_RECVMBOX_SIZE=6 -CONFIG_TCP_QUEUE_OOSEQ=y -CONFIG_TCP_OVERSIZE_MSS=y -# CONFIG_TCP_OVERSIZE_QUARTER_MSS is not set -# CONFIG_TCP_OVERSIZE_DISABLE is not set -CONFIG_UDP_RECVMBOX_SIZE=6 -CONFIG_TCPIP_TASK_STACK_SIZE=3072 -CONFIG_TCPIP_TASK_AFFINITY_NO_AFFINITY=y -# CONFIG_TCPIP_TASK_AFFINITY_CPU0 is not set -CONFIG_TCPIP_TASK_AFFINITY=0x7FFFFFFF -# CONFIG_PPP_SUPPORT is not set -CONFIG_NEWLIB_STDOUT_LINE_ENDING_CRLF=y -# CONFIG_NEWLIB_STDOUT_LINE_ENDING_LF is not set -# CONFIG_NEWLIB_STDOUT_LINE_ENDING_CR is not set -# CONFIG_NEWLIB_STDIN_LINE_ENDING_CRLF is not set -# CONFIG_NEWLIB_STDIN_LINE_ENDING_LF is not set -CONFIG_NEWLIB_STDIN_LINE_ENDING_CR=y # CONFIG_NEWLIB_NANO_FORMAT is not set CONFIG_NEWLIB_TIME_SYSCALL_USE_RTC_HRT=y CONFIG_ESP32C3_TIME_SYSCALL_USE_RTC_SYSTIMER=y @@ -2218,7 +1515,4 @@ CONFIG_ESP32_PTHREAD_TASK_NAME_DEFAULT="pthread" CONFIG_SPI_FLASH_WRITING_DANGEROUS_REGIONS_ABORTS=y # CONFIG_SPI_FLASH_WRITING_DANGEROUS_REGIONS_FAILS is not set # CONFIG_SPI_FLASH_WRITING_DANGEROUS_REGIONS_ALLOWED is not set -CONFIG_SUPPRESS_SELECT_DEBUG_OUTPUT=y -CONFIG_SUPPORT_TERMIOS=y -CONFIG_SEMIHOSTFS_MAX_MOUNT_POINTS=1 # End of deprecated options From b8cba9f3b8992a5f183919a81a96d1314e66d33d Mon Sep 17 00:00:00 2001 From: robcholz <84130577+robcholz@users.noreply.github.com> Date: Tue, 23 Dec 2025 20:43:01 -0500 Subject: [PATCH 3/5] feat: added mc side mod --- feature/mc_mod/build.gradle | 1 + .../com/robcholz/lumen/LumenSyncServer.java | 121 ------- .../com/robcholz/lumen/LumenSyncState.java | 148 ++++++--- .../com/robcholz/lumen/SerialPackClient.java | 70 ++++ .../robcholz/lumen/client/LumenClient.java | 62 +++- .../lumen/client/LumenModMenuIntegration.java | 12 + .../lumen/client/LumenSerialManager.java | 83 +++++ .../lumen/client/SerialPortLocator.java | 69 ++++ .../lumen/client/config/LumenConfig.java | 67 ++++ .../client/config/LumenConfigManager.java | 23 ++ .../client/config/LumenConfigScreen.java | 299 ++++++++++++++++++ .../mc_mod/src/main/resources/fabric.mod.json | 3 + 12 files changed, 784 insertions(+), 174 deletions(-) delete mode 100644 feature/mc_mod/src/client/java/com/robcholz/lumen/LumenSyncServer.java create mode 100644 feature/mc_mod/src/client/java/com/robcholz/lumen/SerialPackClient.java create mode 100644 feature/mc_mod/src/client/java/com/robcholz/lumen/client/LumenModMenuIntegration.java create mode 100644 feature/mc_mod/src/client/java/com/robcholz/lumen/client/LumenSerialManager.java create mode 100644 feature/mc_mod/src/client/java/com/robcholz/lumen/client/SerialPortLocator.java create mode 100644 feature/mc_mod/src/client/java/com/robcholz/lumen/client/config/LumenConfig.java create mode 100644 feature/mc_mod/src/client/java/com/robcholz/lumen/client/config/LumenConfigManager.java create mode 100644 feature/mc_mod/src/client/java/com/robcholz/lumen/client/config/LumenConfigScreen.java diff --git a/feature/mc_mod/build.gradle b/feature/mc_mod/build.gradle index f66176b..177dd69 100644 --- a/feature/mc_mod/build.gradle +++ b/feature/mc_mod/build.gradle @@ -37,6 +37,7 @@ dependencies { modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}" modImplementation "com.terraformersmc:modmenu:${project.modmenu_version}" + implementation "com.fazecast:jSerialComm:2.11.0" } processResources { diff --git a/feature/mc_mod/src/client/java/com/robcholz/lumen/LumenSyncServer.java b/feature/mc_mod/src/client/java/com/robcholz/lumen/LumenSyncServer.java deleted file mode 100644 index e86fd10..0000000 --- a/feature/mc_mod/src/client/java/com/robcholz/lumen/LumenSyncServer.java +++ /dev/null @@ -1,121 +0,0 @@ -package com.robcholz.lumen; - -import com.google.gson.JsonObject; -import com.sun.net.httpserver.Headers; -import com.sun.net.httpserver.HttpExchange; -import com.sun.net.httpserver.HttpServer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.io.OutputStream; -import java.net.Inet4Address; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.NetworkInterface; -import java.nio.charset.StandardCharsets; -import java.util.Enumeration; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -public final class LumenSyncServer { - private static final Logger LOGGER = LoggerFactory.getLogger(LumenSyncServer.class); - private static final int PORT = 47123; - - private static HttpServer server; - - private LumenSyncServer() { - } - - public static synchronized void start() throws IOException { - if (server != null) { - return; - } - - InetAddress bindAddress = resolveBindAddress(); - server = HttpServer.create(new InetSocketAddress(bindAddress, PORT), 0); - server.createContext("/lumen/sync", LumenSyncServer::handleSync); - server.createContext("/lumen/sync/skin", LumenSyncServer::handleSkin); - ExecutorService executor = Executors.newSingleThreadExecutor(r -> { - Thread thread = new Thread(r, "lumen-sync-http"); - thread.setDaemon(true); - return thread; - }); - server.setExecutor(executor); - server.start(); - String host = server.getAddress().getAddress().getHostAddress(); - LOGGER.info("Lumen sync HTTP server listening on http://{}:{}/lumen/sync", host, PORT); - LOGGER.info("Lumen sync HTTP server listening on http://{}:{}/lumen/sync/skin", host, PORT); - } - - private static void handleSync(HttpExchange exchange) throws IOException { - try { - if (!"GET".equalsIgnoreCase(exchange.getRequestMethod())) { - sendJson(exchange, 405, "{\"error\":\"method_not_allowed\"}"); - return; - } - - LumenSyncState.Snapshot snapshot = LumenSyncState.requestSnapshot(false); - JsonObject payload = new JsonObject(); - payload.addProperty("mode", snapshot.mode()); - payload.addProperty("health", snapshot.health()); - payload.addProperty("max_health", snapshot.maxHealth()); - sendJson(exchange, 200, payload.toString()); - } catch (Exception e) { - LOGGER.warn("Failed to handle /lumen/sync request", e); - sendJson(exchange, 500, "{\"error\":\"server_error\"}"); - } finally { - exchange.close(); - } - } - - private static void handleSkin(HttpExchange exchange) throws IOException { - try { - if (!"GET".equalsIgnoreCase(exchange.getRequestMethod())) { - sendJson(exchange, 405, "{\"error\":\"method_not_allowed\"}"); - return; - } - - LumenSyncState.Snapshot snapshot = LumenSyncState.requestSnapshot(true); - JsonObject payload = new JsonObject(); - payload.addProperty("skin_width", snapshot.skinWidth()); - payload.addProperty("skin_height", snapshot.skinHeight()); - payload.addProperty("skin", snapshot.skinBase64()); - sendJson(exchange, 200, payload.toString()); - } catch (Exception e) { - LOGGER.warn("Failed to handle /lumen/sync/skin request", e); - sendJson(exchange, 500, "{\"error\":\"server_error\"}"); - } finally { - exchange.close(); - } - } - - private static void sendJson(HttpExchange exchange, int status, String body) throws IOException { - Headers headers = exchange.getResponseHeaders(); - headers.set("Content-Type", "application/json; charset=utf-8"); - byte[] payload = body.getBytes(StandardCharsets.UTF_8); - exchange.sendResponseHeaders(status, payload.length); - try (OutputStream output = exchange.getResponseBody()) { - output.write(payload); - } - } - - private static InetAddress resolveBindAddress() throws IOException { - Enumeration interfaces = NetworkInterface.getNetworkInterfaces(); - while (interfaces.hasMoreElements()) { - NetworkInterface iface = interfaces.nextElement(); - if (!iface.isUp() || iface.isLoopback() || iface.isVirtual()) { - continue; - } - Enumeration addresses = iface.getInetAddresses(); - while (addresses.hasMoreElements()) { - InetAddress address = addresses.nextElement(); - if (address instanceof Inet4Address && !address.isLoopbackAddress()) { - return address; - } - } - } - LOGGER.warn("No non-loopback IPv4 address found; binding to 0.0.0.0"); - return InetAddress.getByName("0.0.0.0"); - } -} diff --git a/feature/mc_mod/src/client/java/com/robcholz/lumen/LumenSyncState.java b/feature/mc_mod/src/client/java/com/robcholz/lumen/LumenSyncState.java index 327e781..b9c207c 100644 --- a/feature/mc_mod/src/client/java/com/robcholz/lumen/LumenSyncState.java +++ b/feature/mc_mod/src/client/java/com/robcholz/lumen/LumenSyncState.java @@ -12,27 +12,24 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Base64; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; public final class LumenSyncState { private static final Logger LOGGER = LoggerFactory.getLogger(LumenSyncState.class); + private static final int SKIN_HEIGHT = 120; private LumenSyncState() { } - public static Snapshot requestSnapshot(boolean includeSkin) { + public static Snapshot requestPlayerSnapshot() { MinecraftClient client = MinecraftClient.getInstance(); if (client == null) { return Snapshot.defaultSnapshot(); } CompletableFuture future = new CompletableFuture<>(); - client.execute(() -> future.complete(captureSnapshot(client, includeSkin))); + client.execute(() -> future.complete(capturePlayerSnapshot(client))); try { return future.get(2, TimeUnit.SECONDS); } catch (Exception e) { @@ -41,7 +38,22 @@ public static Snapshot requestSnapshot(boolean includeSkin) { } } - private static Snapshot captureSnapshot(MinecraftClient client, boolean includeSkin) { + public static SkinPayload requestSkinPayload() { + MinecraftClient client = MinecraftClient.getInstance(); + if (client == null) { + return SkinPayload.empty(); + } + CompletableFuture future = new CompletableFuture<>(); + client.execute(() -> future.complete(captureSkinPayload(client))); + try { + return future.get(5, TimeUnit.SECONDS); + } catch (Exception e) { + LOGGER.debug("Failed to capture skin payload", e); + return SkinPayload.empty(); + } + } + + private static Snapshot capturePlayerSnapshot(MinecraftClient client) { if (client.player == null) { return Snapshot.defaultSnapshot(); } @@ -50,64 +62,88 @@ private static Snapshot captureSnapshot(MinecraftClient client, boolean includeS String mode = resolveMode(client); double health = player.getHealth(); double maxHealth = player.getMaxHealth(); - SkinData skin = includeSkin ? readSkin(client, player) : SkinData.empty(); + return new Snapshot(mode, health, maxHealth); + } - return new Snapshot(mode, health, maxHealth, skin.width(), skin.height(), skin.base64()); + private static SkinPayload captureSkinPayload(MinecraftClient client) { + if (client.player == null) { + return SkinPayload.empty(); + } + return readSkin(client, client.player); } private static String resolveMode(MinecraftClient client) { if (client.interactionManager == null) { - return "survival"; + return "-----"; } GameMode mode = client.interactionManager.getCurrentGameMode(); if (mode == null) { - return "survival"; + return "-----"; } return switch (mode) { - case CREATIVE -> "creative"; - case ADVENTURE -> "adventure"; - case SPECTATOR -> "spectator"; - default -> "survival"; + case CREATIVE -> "Creative"; + case ADVENTURE -> "Adventure"; + case SPECTATOR -> "Spectator"; + default -> "Survival"; }; } - private static SkinData readSkin(MinecraftClient client, PlayerEntity player) { + private static byte[] toRGB565(NativeImage image) { + int width = image.getWidth(); + int height = image.getHeight(); + byte[] data = new byte[width * height * 2]; + int idx = 0; + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int color = image.getColorArgb(x, y); + int r = (color >>> 16) & 0xFF; + int g = (color >>> 8) & 0xFF; + int b = color & 0xFF; + int value = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >>> 3); + data[idx++] = (byte) (value & 0xFF); + data[idx++] = (byte) ((value >>> 8) & 0xFF); // little-endian + } + } + return data; + } + + private static NativeImage scaleImage(NativeImage image) { + int srcWidth = image.getWidth(); + int srcHeight = image.getHeight(); + int dstHeight = SKIN_HEIGHT; + int dstWidth = Math.max(1, Math.round((srcWidth * (float) dstHeight) / srcHeight)); + NativeImage scaled = new NativeImage(dstWidth, dstHeight, true); + for (int y = 0; y < dstHeight; y++) { + int srcY = (y * srcHeight) / dstHeight; + for (int x = 0; x < dstWidth; x++) { + int srcX = (x * srcWidth) / dstWidth; + int color = image.getColorArgb(srcX, srcY); + scaled.setColorArgb(x, y, color); + } + } + return scaled; + } + + private static SkinPayload readSkin(MinecraftClient client, PlayerEntity player) { if (!(player instanceof AbstractClientPlayerEntity clientPlayer)) { - return SkinData.empty(); + return SkinPayload.empty(); } SkinTextures textures = clientPlayer.getSkinTextures(); if (textures == null || textures.texture() == null) { - return SkinData.empty(); + return SkinPayload.empty(); } AbstractTexture texture = client.getTextureManager().getTexture(textures.texture()); if (texture instanceof ResourceTexture resourceTexture) { Optional image = encodeResourceTexture(client, resourceTexture); if (image.isEmpty()) { - return SkinData.empty(); + return SkinPayload.empty(); } NativeImage frontView = image.get(); - Path tempFile = null; - try { - tempFile = Files.createTempFile("lumen-skin-front", ".png"); - frontView.writeTo(tempFile); - byte[] bytes = Files.readAllBytes(tempFile); - String base64 = Base64.getEncoder().encodeToString(bytes); - return new SkinData(frontView.getWidth(), frontView.getHeight(), base64); - } catch (IOException e) { - LOGGER.debug("Failed to encode front view skin", e); - return SkinData.empty(); - } finally { - frontView.close(); - if (tempFile != null) { - try { - Files.deleteIfExists(tempFile); - } catch (IOException e) { - LOGGER.debug("Failed to delete temp skin file", e); - } - } - } + NativeImage scaled = scaleImage(frontView); + byte[] bytes = toRGB565(scaled); + return new SkinPayload(scaled.getWidth(), scaled.getHeight(), bytes); } - return SkinData.empty(); + return SkinPayload.empty(); } private static Optional encodeFrontView(NativeImage skin) { @@ -233,19 +269,35 @@ private static void blitAlpha(NativeImage src, int sx, int sy, int w, int h, Nat public record Snapshot( String mode, double health, - double maxHealth, - int skinWidth, - int skinHeight, - String skinBase64 + double maxHealth ) { public static Snapshot defaultSnapshot() { - return new Snapshot("survival", 0.0, 0.0, 0, 0, ""); + return new Snapshot("-----", 0.0, 0.0); } } - private record SkinData(int width, int height, String base64) { - public static SkinData empty() { - return new SkinData(0, 0, ""); + public record SkinPayload(int width, int height, byte[] rgb565) { + public static SkinPayload empty() { + return new SkinPayload(0, 0, new byte[0]); + } + + public byte[] toWireBytes() { + if (rgb565.length == 0) { + return new byte[0]; + } + byte[] data = new byte[4 + rgb565.length]; + data[0] = (byte) (width & 0xFF); + data[1] = (byte) ((width >>> 8) & 0xFF); + data[2] = (byte) (height & 0xFF); + data[3] = (byte) ((height >>> 8) & 0xFF); + + for (int i = 0; i < rgb565.length; i += 2) { + byte lsb = rgb565[i]; + byte msb = rgb565[i + 1]; + data[4 + i] = msb; + data[4 + i + 1] = lsb; + } + return data; } } } diff --git a/feature/mc_mod/src/client/java/com/robcholz/lumen/SerialPackClient.java b/feature/mc_mod/src/client/java/com/robcholz/lumen/SerialPackClient.java new file mode 100644 index 0000000..1dab37a --- /dev/null +++ b/feature/mc_mod/src/client/java/com/robcholz/lumen/SerialPackClient.java @@ -0,0 +1,70 @@ +package com.robcholz.lumen; + +import com.fazecast.jSerialComm.SerialPort; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +public class SerialPackClient implements AutoCloseable { + private final SerialPort port; + private final String portPath; + + public SerialPackClient(String portPath) throws IOException { + this.portPath = portPath; + port = SerialPort.getCommPort(portPath); + if (port == null) { + throw new IOException("Serial port not found: " + portPath); + } + port.setBaudRate(460800); + port.setComPortTimeouts(SerialPort.TIMEOUT_WRITE_BLOCKING, 0, 0); + if (!port.openPort()) { + throw new IOException("Failed to open serial port: " + portPath); + } + } + + private static byte[] encodeU32(int value) { + return new byte[]{ + (byte) (value & 0xFF), + (byte) ((value >>> 8) & 0xFF), + (byte) ((value >>> 16) & 0xFF), + (byte) ((value >>> 24) & 0xFF) + }; + } + + public synchronized void send(String path, byte[] data) throws IOException { + if (!port.isOpen()) { + throw new IOException("Serial port is closed"); + } + byte[] pathBytes = path.getBytes(StandardCharsets.UTF_8); + writeAll(pathBytes); + writeAll(new byte[]{'\n'}); + writeAll(encodeU32(data.length)); + writeAll(data); + } + + public boolean isOpen() { + return port.isOpen(); + } + + public String getPortPath() { + return portPath; + } + + private void writeAll(byte[] bytes) throws IOException { + int offset = 0; + while (offset < bytes.length) { + int written = port.writeBytes(bytes, bytes.length - offset, offset); + if (written <= 0) { + throw new IOException("Serial write failed"); + } + offset += written; + } + } + + @Override + public void close() { + if (port.isOpen()) { + port.closePort(); + } + } +} diff --git a/feature/mc_mod/src/client/java/com/robcholz/lumen/client/LumenClient.java b/feature/mc_mod/src/client/java/com/robcholz/lumen/client/LumenClient.java index 9f784dc..01dce39 100644 --- a/feature/mc_mod/src/client/java/com/robcholz/lumen/client/LumenClient.java +++ b/feature/mc_mod/src/client/java/com/robcholz/lumen/client/LumenClient.java @@ -1,21 +1,73 @@ package com.robcholz.lumen.client; -import com.robcholz.lumen.LumenSyncServer; +import com.robcholz.lumen.LumenSyncState; +import com.robcholz.lumen.client.config.LumenConfig; +import com.robcholz.lumen.client.config.LumenConfigManager; import net.fabricmc.api.ClientModInitializer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + public class LumenClient implements ClientModInitializer { public static final String MOD_ID = "lumen"; public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); + private static final ScheduledExecutorService SERIAL_EXECUTOR = + Executors.newSingleThreadScheduledExecutor(r -> { + Thread thread = new Thread(r, "lumen-serial-sync"); + thread.setDaemon(true); + return thread; + }); - @Override - public void onInitializeClient() { + private static void sendPlayerInfo(LumenSerialManager serial) { + try { + LumenSyncState.Snapshot snapshot = LumenSyncState.requestPlayerSnapshot(); + String json = "{\"mode\":\"" + snapshot.mode() + "\",\"health\":" + + snapshot.health() + ",\"max_health\":" + snapshot.maxHealth() + "}"; + serial.send("sync", json.getBytes(StandardCharsets.UTF_8)); + } catch (Exception e) { + LOGGER.debug("Failed to send player info over serial", e); + } + } + + private static void sendSkinInfo(LumenSerialManager serial) { try { - LumenSyncServer.start(); + LumenSyncState.SkinPayload payload = LumenSyncState.requestSkinPayload(); + byte[] data = payload.toWireBytes(); + if (data.length == 0) { + return; + } + serial.send("sync/skin", data); } catch (Exception e) { - LOGGER.error("Failed to start Lumen sync HTTP server", e); + LOGGER.debug("Failed to send skin info over serial", e); } + } + + @Override + public void onInitializeClient() { + LumenConfig config = LumenConfigManager.get(); + String portPath = SerialPortLocator.resolvePortPath(config); + if (portPath == null || portPath.isBlank()) { + LOGGER.warn("No serial port configured; set it in Mod Menu or via -Dlumen.serialPort/LUMEN_SERIAL_PORT"); + } + + LumenSerialManager serial = new LumenSerialManager(); + SERIAL_EXECUTOR.scheduleAtFixedRate( + () -> sendPlayerInfo(serial), + 0, + 1, + TimeUnit.SECONDS + ); + SERIAL_EXECUTOR.scheduleAtFixedRate( + () -> sendSkinInfo(serial), + 0, + 20, + TimeUnit.SECONDS + ); + LOGGER.info("Lumen client ready"); } } diff --git a/feature/mc_mod/src/client/java/com/robcholz/lumen/client/LumenModMenuIntegration.java b/feature/mc_mod/src/client/java/com/robcholz/lumen/client/LumenModMenuIntegration.java new file mode 100644 index 0000000..d612746 --- /dev/null +++ b/feature/mc_mod/src/client/java/com/robcholz/lumen/client/LumenModMenuIntegration.java @@ -0,0 +1,12 @@ +package com.robcholz.lumen.client; + +import com.robcholz.lumen.client.config.LumenConfigScreen; +import com.terraformersmc.modmenu.api.ConfigScreenFactory; +import com.terraformersmc.modmenu.api.ModMenuApi; + +public class LumenModMenuIntegration implements ModMenuApi { + @Override + public ConfigScreenFactory getModConfigScreenFactory() { + return LumenConfigScreen::new; + } +} diff --git a/feature/mc_mod/src/client/java/com/robcholz/lumen/client/LumenSerialManager.java b/feature/mc_mod/src/client/java/com/robcholz/lumen/client/LumenSerialManager.java new file mode 100644 index 0000000..4675b32 --- /dev/null +++ b/feature/mc_mod/src/client/java/com/robcholz/lumen/client/LumenSerialManager.java @@ -0,0 +1,83 @@ +package com.robcholz.lumen.client; + +import com.robcholz.lumen.SerialPackClient; +import com.robcholz.lumen.client.config.LumenConfig; +import com.robcholz.lumen.client.config.LumenConfigManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +public final class LumenSerialManager implements AutoCloseable { + private static final Logger LOGGER = LoggerFactory.getLogger("lumen"); + + private SerialPackClient client; + private String connectedPortPath; + private long lastReconnectAttemptMillis; + private boolean hasAttemptedInitialConnect; + + public void send(String path, byte[] data) { + if (!ensureConnected()) { + return; + } + try { + client.send(path, data); + } catch (IOException e) { + LOGGER.debug("Serial send failed, closing connection", e); + closeClient(); + } + } + + private boolean ensureConnected() { + LumenConfig config = LumenConfigManager.get(); + String desiredPortPath = SerialPortLocator.resolvePortPath(config); + if (desiredPortPath == null || desiredPortPath.isBlank()) { + return false; + } + if (client != null && !client.isOpen()) { + closeClient(); + } + if (client != null && !desiredPortPath.equals(connectedPortPath)) { + closeClient(); + } + if (client != null) { + return true; + } + + long now = System.currentTimeMillis(); + if (hasAttemptedInitialConnect) { + if (!config.autoReconnect) { + return false; + } + long intervalMillis = Math.max(1, config.reconnectPeriodSeconds) * 1000L; + if (now - lastReconnectAttemptMillis < intervalMillis) { + return false; + } + } + + hasAttemptedInitialConnect = true; + lastReconnectAttemptMillis = now; + try { + client = new SerialPackClient(desiredPortPath); + connectedPortPath = desiredPortPath; + return true; + } catch (IOException e) { + LOGGER.debug("Failed to connect to serial port {}", desiredPortPath, e); + closeClient(); + return false; + } + } + + private void closeClient() { + if (client != null) { + client.close(); + } + client = null; + connectedPortPath = null; + } + + @Override + public void close() { + closeClient(); + } +} diff --git a/feature/mc_mod/src/client/java/com/robcholz/lumen/client/SerialPortLocator.java b/feature/mc_mod/src/client/java/com/robcholz/lumen/client/SerialPortLocator.java new file mode 100644 index 0000000..b2bd15c --- /dev/null +++ b/feature/mc_mod/src/client/java/com/robcholz/lumen/client/SerialPortLocator.java @@ -0,0 +1,69 @@ +package com.robcholz.lumen.client; + +import com.fazecast.jSerialComm.SerialPort; +import com.robcholz.lumen.client.config.LumenConfig; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public final class SerialPortLocator { + private SerialPortLocator() { + } + + public static String resolvePortPath(LumenConfig config) { + String configured = normalize(config.portPath); + if (!configured.isEmpty()) { + return configured; + } + String property = normalize(System.getProperty("lumen.serialPort")); + if (!property.isEmpty()) { + return property; + } + String env = normalize(System.getenv("LUMEN_SERIAL_PORT")); + if (!env.isEmpty()) { + return env; + } + return autoDetectPort(); + } + + public static String autoDetectPort() { + SerialPort[] ports = SerialPort.getCommPorts(); + if (ports.length == 0) { + return null; + } + String best = null; + for (SerialPort port : ports) { + String name = normalize(port.getSystemPortName()); + String lower = name.toLowerCase(); + if (lower.contains("usb") || lower.contains("modem") || lower.contains("tty")) { + best = port.getSystemPortName(); + break; + } + } + if (best != null) { + return best; + } + return ports[0].getSystemPortName(); + } + + public static List listPorts() { + SerialPort[] ports = SerialPort.getCommPorts(); + List names = new ArrayList<>(ports.length); + for (SerialPort port : ports) { + String name = normalize(port.getSystemPortName()); + if (!name.isEmpty()) { + names.add(name); + } + } + Collections.sort(names); + return names; + } + + private static String normalize(String value) { + if (value == null) { + return ""; + } + return value.trim(); + } +} diff --git a/feature/mc_mod/src/client/java/com/robcholz/lumen/client/config/LumenConfig.java b/feature/mc_mod/src/client/java/com/robcholz/lumen/client/config/LumenConfig.java new file mode 100644 index 0000000..0ba7ffe --- /dev/null +++ b/feature/mc_mod/src/client/java/com/robcholz/lumen/client/config/LumenConfig.java @@ -0,0 +1,67 @@ +package com.robcholz.lumen.client.config; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; +import java.nio.file.Files; +import java.nio.file.Path; + +public class LumenConfig { + private static final Logger LOGGER = LoggerFactory.getLogger("lumen"); + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + + public String portPath = ""; + public boolean autoReconnect = true; + public int reconnectPeriodSeconds = 5; + + public static LumenConfig load(Path path) { + if (!Files.exists(path)) { + return new LumenConfig(); + } + try (Reader reader = Files.newBufferedReader(path)) { + LumenConfig config = GSON.fromJson(reader, LumenConfig.class); + if (config == null) { + return new LumenConfig(); + } + config.normalize(); + return config; + } catch (IOException e) { + LOGGER.warn("Failed to load config from {}", path, e); + return new LumenConfig(); + } + } + + public LumenConfig copy() { + LumenConfig copy = new LumenConfig(); + copy.portPath = portPath; + copy.autoReconnect = autoReconnect; + copy.reconnectPeriodSeconds = reconnectPeriodSeconds; + return copy; + } + + public void save(Path path) { + normalize(); + try { + Files.createDirectories(path.getParent()); + try (Writer writer = Files.newBufferedWriter(path)) { + GSON.toJson(this, writer); + } + } catch (IOException e) { + LOGGER.warn("Failed to save config to {}", path, e); + } + } + + private void normalize() { + if (portPath == null) { + portPath = ""; + } + if (reconnectPeriodSeconds < 1) { + reconnectPeriodSeconds = 1; + } + } +} diff --git a/feature/mc_mod/src/client/java/com/robcholz/lumen/client/config/LumenConfigManager.java b/feature/mc_mod/src/client/java/com/robcholz/lumen/client/config/LumenConfigManager.java new file mode 100644 index 0000000..e93f177 --- /dev/null +++ b/feature/mc_mod/src/client/java/com/robcholz/lumen/client/config/LumenConfigManager.java @@ -0,0 +1,23 @@ +package com.robcholz.lumen.client.config; + +import net.fabricmc.loader.api.FabricLoader; + +import java.nio.file.Path; + +public final class LumenConfigManager { + private static final String FILE_NAME = "lumen.json"; + private static final Path CONFIG_PATH = FabricLoader.getInstance().getConfigDir().resolve(FILE_NAME); + private static volatile LumenConfig config = LumenConfig.load(CONFIG_PATH); + + private LumenConfigManager() { + } + + public static LumenConfig get() { + return config; + } + + public static void save(LumenConfig updated) { + config = updated; + updated.save(CONFIG_PATH); + } +} diff --git a/feature/mc_mod/src/client/java/com/robcholz/lumen/client/config/LumenConfigScreen.java b/feature/mc_mod/src/client/java/com/robcholz/lumen/client/config/LumenConfigScreen.java new file mode 100644 index 0000000..41e6748 --- /dev/null +++ b/feature/mc_mod/src/client/java/com/robcholz/lumen/client/config/LumenConfigScreen.java @@ -0,0 +1,299 @@ +package com.robcholz.lumen.client.config; + +import com.robcholz.lumen.client.SerialPortLocator; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.gui.widget.TextFieldWidget; +import net.minecraft.text.Text; + +import java.util.List; + +public class LumenConfigScreen extends Screen { + private static final int ENTRY_HEIGHT = 20; + private static final int LIST_PADDING = 2; + private final Screen parent; + private final LumenConfig workingConfig; + private TextFieldWidget portField; + private TextFieldWidget reconnectField; + private boolean autoReconnectEnabled; + private ButtonWidget autoReconnectButton; + private ButtonWidget portSelectButton; + private ButtonWidget refreshPortsButton; + private List portOptions = List.of(); + private boolean showPortList; + private String lastPortFieldValue = ""; + private int portListX; + private int portListY; + private int portListWidth; + private int scrollOffset; + + public LumenConfigScreen(Screen parent) { + super(Text.literal("Lumen")); + this.parent = parent; + this.workingConfig = LumenConfigManager.get().copy(); + } + + @Override + protected void init() { + int centerX = width / 2; + int y = height / 4; + + portField = new TextFieldWidget(textRenderer, centerX - 100, y, 200, 20, Text.literal("Port path")); + portField.setMaxLength(256); + portField.setText(workingConfig.portPath); + addSelectableChild(portField); + + y += 26; + portSelectButton = ButtonWidget.builder(portSelectLabel(), button -> { + showPortList = !showPortList; + if (showPortList) { + refreshPortOptions(); + scrollOffset = 0; + } + }).dimensions(centerX - 100, y, 200, 20).build(); + addDrawableChild(portSelectButton); + portListX = centerX - 100; + portListY = y + 22; + portListWidth = 200; + + y += 24; + refreshPortsButton = ButtonWidget.builder(Text.literal("Refresh ports"), button -> { + refreshPortOptions(); + scrollOffset = 0; + }).dimensions(centerX - 100, y, 200, 20).build(); + addDrawableChild(refreshPortsButton); + + y += 24; + addDrawableChild(ButtonWidget.builder(Text.literal("Auto-detect port"), button -> { + String detected = SerialPortLocator.autoDetectPort(); + if (detected != null) { + portField.setText(detected); + } + portSelectButton.setMessage(portSelectLabel()); + }).dimensions(centerX - 100, y, 200, 20).build()); + + y += 30; + autoReconnectEnabled = workingConfig.autoReconnect; + autoReconnectButton = ButtonWidget.builder(autoReconnectLabel(), button -> { + autoReconnectEnabled = !autoReconnectEnabled; + autoReconnectButton.setMessage(autoReconnectLabel()); + }).dimensions(centerX - 100, y, 200, 20).build(); + addDrawableChild(autoReconnectButton); + + y += 26; + reconnectField = new TextFieldWidget(textRenderer, centerX - 100, y, 200, 20, Text.literal("Reconnect period (sec)")); + reconnectField.setMaxLength(4); + reconnectField.setText(Integer.toString(workingConfig.reconnectPeriodSeconds)); + addSelectableChild(reconnectField); + + int buttonY = height - 50; + addDrawableChild(ButtonWidget.builder(Text.literal("Done"), button -> saveAndClose()) + .dimensions(centerX - 100, buttonY, 95, 20) + .build()); + addDrawableChild(ButtonWidget.builder(Text.literal("Cancel"), button -> close()) + .dimensions(centerX + 5, buttonY, 95, 20) + .build()); + + refreshPortOptions(); + setInitialFocus(portField); + } + + @Override + public void close() { + if (client != null) { + client.setScreen(parent); + } + } + + private void saveAndClose() { + String port = portField.getText().trim(); + workingConfig.portPath = port; + workingConfig.autoReconnect = autoReconnectEnabled; + workingConfig.reconnectPeriodSeconds = parseReconnectPeriod(reconnectField.getText()); + LumenConfigManager.save(workingConfig); + close(); + } + + private int parseReconnectPeriod(String text) { + try { + int value = Integer.parseInt(text.trim()); + return Math.max(1, value); + } catch (NumberFormatException e) { + return Math.max(1, workingConfig.reconnectPeriodSeconds); + } + } + + @Override + public void render(net.minecraft.client.gui.DrawContext context, int mouseX, int mouseY, float delta) { + renderBackground(context, mouseX, mouseY, delta); + context.drawCenteredTextWithShadow(textRenderer, Text.literal("Lumen Serial Settings"), width / 2, 20, 0xFFFFFF); + context.drawTextWithShadow(textRenderer, Text.literal("Leave port blank to auto-detect"), width / 2 - 100, height / 4 - 12, 0xAAAAAA); + super.render(context, mouseX, mouseY, delta); + if (showPortList) { + context.getMatrices().push(); + context.getMatrices().translate(0, 0, 400); + renderPortList(context, mouseX, mouseY); + context.getMatrices().pop(); + } + } + + @Override + public void tick() { + super.tick(); + String current = portField.getText(); + if (!current.equals(lastPortFieldValue)) { + lastPortFieldValue = current; + portSelectButton.setMessage(portSelectLabel()); + } + } + + private Text autoReconnectLabel() { + return Text.literal("Auto-reconnect: " + (autoReconnectEnabled ? "On" : "Off")); + } + + private Text portSelectLabel() { + String value = portField.getText().trim(); + if (value.isEmpty()) { + value = "Auto-detect"; + } + return Text.literal("Select port: " + value); + } + + private void refreshPortOptions() { + portOptions = SerialPortLocator.listPorts(); + } + + private void renderPortList(net.minecraft.client.gui.DrawContext context, int mouseX, int mouseY) { + int listSize = totalPortEntries(); + int visibleEntries = Math.min(listSize, maxVisibleEntries()); + int listHeight = visibleEntries * ENTRY_HEIGHT + LIST_PADDING * 2; + int x1 = portListX; + int y1 = portListY; + int x2 = portListX + portListWidth; + int y2 = portListY + listHeight; + + context.fill(x1, y1, x2, y2, 0xFF3A3A3A); + context.fill(x1 + 1, y1 + 1, x2 - 1, y2 - 1, 0xFF111111); + + int startIndex = clampScrollOffset(visibleEntries, listSize); + int endIndex = Math.min(listSize, startIndex + visibleEntries); + for (int index = startIndex; index < endIndex; index++) { + String label = portLabelForIndex(index); + drawPortEntry(context, mouseX, mouseY, index - startIndex, label); + } + + if (listSize > visibleEntries) { + drawScrollBar(context, x2 - 5, y1 + 1, listHeight - 2, startIndex, listSize, visibleEntries); + } + } + + private void drawPortEntry(net.minecraft.client.gui.DrawContext context, int mouseX, int mouseY, int index, String label) { + int x = portListX + LIST_PADDING; + int y = portListY + LIST_PADDING + index * ENTRY_HEIGHT; + int width = portListWidth - LIST_PADDING * 2; + int height = ENTRY_HEIGHT; + boolean hover = mouseX >= x && mouseX <= x + width && mouseY >= y && mouseY <= y + height; + if (hover) { + context.fill(x, y, x + width, y + height, 0xFF3A3A3A); + } + context.drawTextWithShadow(textRenderer, Text.literal(label), x + 4, y + 6, 0xFFFFFF); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (showPortList) { + if (handlePortListClick(mouseX, mouseY)) { + return true; + } + showPortList = false; + return true; + } + return super.mouseClicked(mouseX, mouseY, button); + } + + private boolean handlePortListClick(double mouseX, double mouseY) { + int listSize = totalPortEntries(); + int visibleEntries = Math.min(listSize, maxVisibleEntries()); + int listHeight = visibleEntries * ENTRY_HEIGHT + LIST_PADDING * 2; + int x1 = portListX + LIST_PADDING; + int y1 = portListY + LIST_PADDING; + int x2 = portListX + portListWidth - LIST_PADDING; + int y2 = portListY + listHeight - LIST_PADDING; + if (mouseX < x1 || mouseX > x2 || mouseY < y1 || mouseY > y2) { + return false; + } + int startIndex = clampScrollOffset(visibleEntries, listSize); + int index = (int) ((mouseY - y1) / ENTRY_HEIGHT) + startIndex; + if (index == 0) { + portField.setText(""); + portSelectButton.setMessage(portSelectLabel()); + showPortList = false; + return true; + } + if (portOptions.isEmpty()) { + return true; + } + int portIndex = index - 1; + if (portIndex >= 0 && portIndex < portOptions.size()) { + portField.setText(portOptions.get(portIndex)); + portSelectButton.setMessage(portSelectLabel()); + showPortList = false; + return true; + } + return false; + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double horizontalAmount, double verticalAmount) { + if (showPortList) { + int listSize = totalPortEntries(); + int visibleEntries = Math.min(listSize, maxVisibleEntries()); + if (listSize > visibleEntries) { + int delta = verticalAmount > 0 ? -1 : 1; + scrollOffset = Math.max(0, Math.min(scrollOffset + delta, listSize - visibleEntries)); + return true; + } + } + return super.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount); + } + + private int totalPortEntries() { + return 1 + Math.max(1, portOptions.size()); + } + + private int maxVisibleEntries() { + int maxPixels = Math.max(ENTRY_HEIGHT * 2, height - portListY - 60); + return Math.max(1, maxPixels / ENTRY_HEIGHT); + } + + private int clampScrollOffset(int visibleEntries, int listSize) { + int maxOffset = Math.max(0, listSize - visibleEntries); + if (scrollOffset > maxOffset) { + scrollOffset = maxOffset; + } + return scrollOffset; + } + + private String portLabelForIndex(int index) { + if (index == 0) { + return "Auto-detect"; + } + if (portOptions.isEmpty()) { + return "No ports found"; + } + int portIndex = index - 1; + if (portIndex >= 0 && portIndex < portOptions.size()) { + return portOptions.get(portIndex); + } + return ""; + } + + private void drawScrollBar(net.minecraft.client.gui.DrawContext context, int x, int y, int height, int startIndex, int listSize, int visibleEntries) { + int trackHeight = height; + int thumbHeight = Math.max(12, (trackHeight * visibleEntries) / listSize); + int maxOffset = Math.max(1, listSize - visibleEntries); + int thumbY = y + (trackHeight - thumbHeight) * startIndex / maxOffset; + context.fill(x, y, x + 3, y + height, 0xFF202020); + context.fill(x, thumbY, x + 3, thumbY + thumbHeight, 0xFF8A8A8A); + } +} diff --git a/feature/mc_mod/src/main/resources/fabric.mod.json b/feature/mc_mod/src/main/resources/fabric.mod.json index 58f5781..48eacfe 100644 --- a/feature/mc_mod/src/main/resources/fabric.mod.json +++ b/feature/mc_mod/src/main/resources/fabric.mod.json @@ -17,6 +17,9 @@ "entrypoints": { "client": [ "com.robcholz.lumen.client.LumenClient" + ], + "modmenu": [ + "com.robcholz.lumen.client.LumenModMenuIntegration" ] }, "mixins": [], From 826f24562d0271d30314c8c7631e921b9c37181e Mon Sep 17 00:00:00 2001 From: robcholz <84130577+robcholz@users.noreply.github.com> Date: Tue, 23 Dec 2025 20:44:25 -0500 Subject: [PATCH 4/5] refactor: moved dir --- {feature => addon}/mc_mod/.gitignore | 0 {feature => addon}/mc_mod/build.gradle | 0 {feature => addon}/mc_mod/gradle.properties | 0 .../mc_mod/gradle/wrapper/gradle-wrapper.jar | Bin .../mc_mod/gradle/wrapper/gradle-wrapper.properties | 0 {feature => addon}/mc_mod/gradlew | 0 {feature => addon}/mc_mod/gradlew.bat | 0 {feature => addon}/mc_mod/settings.gradle | 0 .../java/com/robcholz/lumen/LumenSyncState.java | 0 .../java/com/robcholz/lumen/SerialPackClient.java | 0 .../java/com/robcholz/lumen/client/LumenClient.java | 0 .../lumen/client/LumenModMenuIntegration.java | 0 .../robcholz/lumen/client/LumenSerialManager.java | 0 .../robcholz/lumen/client/SerialPortLocator.java | 0 .../robcholz/lumen/client/config/LumenConfig.java | 0 .../lumen/client/config/LumenConfigManager.java | 0 .../lumen/client/config/LumenConfigScreen.java | 0 .../mc_mod/src/main/resources/assets/lumen/icon.png | Bin .../mc_mod/src/main/resources/fabric.mod.json | 0 19 files changed, 0 insertions(+), 0 deletions(-) rename {feature => addon}/mc_mod/.gitignore (100%) rename {feature => addon}/mc_mod/build.gradle (100%) rename {feature => addon}/mc_mod/gradle.properties (100%) rename {feature => addon}/mc_mod/gradle/wrapper/gradle-wrapper.jar (100%) rename {feature => addon}/mc_mod/gradle/wrapper/gradle-wrapper.properties (100%) rename {feature => addon}/mc_mod/gradlew (100%) rename {feature => addon}/mc_mod/gradlew.bat (100%) rename {feature => addon}/mc_mod/settings.gradle (100%) rename {feature => addon}/mc_mod/src/client/java/com/robcholz/lumen/LumenSyncState.java (100%) rename {feature => addon}/mc_mod/src/client/java/com/robcholz/lumen/SerialPackClient.java (100%) rename {feature => addon}/mc_mod/src/client/java/com/robcholz/lumen/client/LumenClient.java (100%) rename {feature => addon}/mc_mod/src/client/java/com/robcholz/lumen/client/LumenModMenuIntegration.java (100%) rename {feature => addon}/mc_mod/src/client/java/com/robcholz/lumen/client/LumenSerialManager.java (100%) rename {feature => addon}/mc_mod/src/client/java/com/robcholz/lumen/client/SerialPortLocator.java (100%) rename {feature => addon}/mc_mod/src/client/java/com/robcholz/lumen/client/config/LumenConfig.java (100%) rename {feature => addon}/mc_mod/src/client/java/com/robcholz/lumen/client/config/LumenConfigManager.java (100%) rename {feature => addon}/mc_mod/src/client/java/com/robcholz/lumen/client/config/LumenConfigScreen.java (100%) rename {feature => addon}/mc_mod/src/main/resources/assets/lumen/icon.png (100%) rename {feature => addon}/mc_mod/src/main/resources/fabric.mod.json (100%) diff --git a/feature/mc_mod/.gitignore b/addon/mc_mod/.gitignore similarity index 100% rename from feature/mc_mod/.gitignore rename to addon/mc_mod/.gitignore diff --git a/feature/mc_mod/build.gradle b/addon/mc_mod/build.gradle similarity index 100% rename from feature/mc_mod/build.gradle rename to addon/mc_mod/build.gradle diff --git a/feature/mc_mod/gradle.properties b/addon/mc_mod/gradle.properties similarity index 100% rename from feature/mc_mod/gradle.properties rename to addon/mc_mod/gradle.properties diff --git a/feature/mc_mod/gradle/wrapper/gradle-wrapper.jar b/addon/mc_mod/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from feature/mc_mod/gradle/wrapper/gradle-wrapper.jar rename to addon/mc_mod/gradle/wrapper/gradle-wrapper.jar diff --git a/feature/mc_mod/gradle/wrapper/gradle-wrapper.properties b/addon/mc_mod/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from feature/mc_mod/gradle/wrapper/gradle-wrapper.properties rename to addon/mc_mod/gradle/wrapper/gradle-wrapper.properties diff --git a/feature/mc_mod/gradlew b/addon/mc_mod/gradlew similarity index 100% rename from feature/mc_mod/gradlew rename to addon/mc_mod/gradlew diff --git a/feature/mc_mod/gradlew.bat b/addon/mc_mod/gradlew.bat similarity index 100% rename from feature/mc_mod/gradlew.bat rename to addon/mc_mod/gradlew.bat diff --git a/feature/mc_mod/settings.gradle b/addon/mc_mod/settings.gradle similarity index 100% rename from feature/mc_mod/settings.gradle rename to addon/mc_mod/settings.gradle diff --git a/feature/mc_mod/src/client/java/com/robcholz/lumen/LumenSyncState.java b/addon/mc_mod/src/client/java/com/robcholz/lumen/LumenSyncState.java similarity index 100% rename from feature/mc_mod/src/client/java/com/robcholz/lumen/LumenSyncState.java rename to addon/mc_mod/src/client/java/com/robcholz/lumen/LumenSyncState.java diff --git a/feature/mc_mod/src/client/java/com/robcholz/lumen/SerialPackClient.java b/addon/mc_mod/src/client/java/com/robcholz/lumen/SerialPackClient.java similarity index 100% rename from feature/mc_mod/src/client/java/com/robcholz/lumen/SerialPackClient.java rename to addon/mc_mod/src/client/java/com/robcholz/lumen/SerialPackClient.java diff --git a/feature/mc_mod/src/client/java/com/robcholz/lumen/client/LumenClient.java b/addon/mc_mod/src/client/java/com/robcholz/lumen/client/LumenClient.java similarity index 100% rename from feature/mc_mod/src/client/java/com/robcholz/lumen/client/LumenClient.java rename to addon/mc_mod/src/client/java/com/robcholz/lumen/client/LumenClient.java diff --git a/feature/mc_mod/src/client/java/com/robcholz/lumen/client/LumenModMenuIntegration.java b/addon/mc_mod/src/client/java/com/robcholz/lumen/client/LumenModMenuIntegration.java similarity index 100% rename from feature/mc_mod/src/client/java/com/robcholz/lumen/client/LumenModMenuIntegration.java rename to addon/mc_mod/src/client/java/com/robcholz/lumen/client/LumenModMenuIntegration.java diff --git a/feature/mc_mod/src/client/java/com/robcholz/lumen/client/LumenSerialManager.java b/addon/mc_mod/src/client/java/com/robcholz/lumen/client/LumenSerialManager.java similarity index 100% rename from feature/mc_mod/src/client/java/com/robcholz/lumen/client/LumenSerialManager.java rename to addon/mc_mod/src/client/java/com/robcholz/lumen/client/LumenSerialManager.java diff --git a/feature/mc_mod/src/client/java/com/robcholz/lumen/client/SerialPortLocator.java b/addon/mc_mod/src/client/java/com/robcholz/lumen/client/SerialPortLocator.java similarity index 100% rename from feature/mc_mod/src/client/java/com/robcholz/lumen/client/SerialPortLocator.java rename to addon/mc_mod/src/client/java/com/robcholz/lumen/client/SerialPortLocator.java diff --git a/feature/mc_mod/src/client/java/com/robcholz/lumen/client/config/LumenConfig.java b/addon/mc_mod/src/client/java/com/robcholz/lumen/client/config/LumenConfig.java similarity index 100% rename from feature/mc_mod/src/client/java/com/robcholz/lumen/client/config/LumenConfig.java rename to addon/mc_mod/src/client/java/com/robcholz/lumen/client/config/LumenConfig.java diff --git a/feature/mc_mod/src/client/java/com/robcholz/lumen/client/config/LumenConfigManager.java b/addon/mc_mod/src/client/java/com/robcholz/lumen/client/config/LumenConfigManager.java similarity index 100% rename from feature/mc_mod/src/client/java/com/robcholz/lumen/client/config/LumenConfigManager.java rename to addon/mc_mod/src/client/java/com/robcholz/lumen/client/config/LumenConfigManager.java diff --git a/feature/mc_mod/src/client/java/com/robcholz/lumen/client/config/LumenConfigScreen.java b/addon/mc_mod/src/client/java/com/robcholz/lumen/client/config/LumenConfigScreen.java similarity index 100% rename from feature/mc_mod/src/client/java/com/robcholz/lumen/client/config/LumenConfigScreen.java rename to addon/mc_mod/src/client/java/com/robcholz/lumen/client/config/LumenConfigScreen.java diff --git a/feature/mc_mod/src/main/resources/assets/lumen/icon.png b/addon/mc_mod/src/main/resources/assets/lumen/icon.png similarity index 100% rename from feature/mc_mod/src/main/resources/assets/lumen/icon.png rename to addon/mc_mod/src/main/resources/assets/lumen/icon.png diff --git a/feature/mc_mod/src/main/resources/fabric.mod.json b/addon/mc_mod/src/main/resources/fabric.mod.json similarity index 100% rename from feature/mc_mod/src/main/resources/fabric.mod.json rename to addon/mc_mod/src/main/resources/fabric.mod.json From ed89e5a3f614d29259a5f27b51f926239ec03616 Mon Sep 17 00:00:00 2001 From: robcholz <84130577+robcholz@users.noreply.github.com> Date: Tue, 23 Dec 2025 20:47:17 -0500 Subject: [PATCH 5/5] refactor: removed --- addon/mc_mod/.gitignore | 119 ------- addon/mc_mod/build.gradle | 93 ------ addon/mc_mod/gradle.properties | 16 - .../mc_mod/gradle/wrapper/gradle-wrapper.jar | Bin 45633 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 - addon/mc_mod/gradlew | 248 -------------- addon/mc_mod/gradlew.bat | 93 ------ addon/mc_mod/settings.gradle | 9 - .../com/robcholz/lumen/LumenSyncState.java | 303 ------------------ .../com/robcholz/lumen/SerialPackClient.java | 70 ---- .../robcholz/lumen/client/LumenClient.java | 73 ----- .../lumen/client/LumenModMenuIntegration.java | 12 - .../lumen/client/LumenSerialManager.java | 83 ----- .../lumen/client/SerialPortLocator.java | 69 ---- .../lumen/client/config/LumenConfig.java | 67 ---- .../client/config/LumenConfigManager.java | 23 -- .../client/config/LumenConfigScreen.java | 299 ----------------- .../src/main/resources/assets/lumen/icon.png | Bin 1689 -> 0 bytes .../mc_mod/src/main/resources/fabric.mod.json | 31 -- 19 files changed, 1615 deletions(-) delete mode 100644 addon/mc_mod/.gitignore delete mode 100644 addon/mc_mod/build.gradle delete mode 100644 addon/mc_mod/gradle.properties delete mode 100644 addon/mc_mod/gradle/wrapper/gradle-wrapper.jar delete mode 100644 addon/mc_mod/gradle/wrapper/gradle-wrapper.properties delete mode 100755 addon/mc_mod/gradlew delete mode 100644 addon/mc_mod/gradlew.bat delete mode 100644 addon/mc_mod/settings.gradle delete mode 100644 addon/mc_mod/src/client/java/com/robcholz/lumen/LumenSyncState.java delete mode 100644 addon/mc_mod/src/client/java/com/robcholz/lumen/SerialPackClient.java delete mode 100644 addon/mc_mod/src/client/java/com/robcholz/lumen/client/LumenClient.java delete mode 100644 addon/mc_mod/src/client/java/com/robcholz/lumen/client/LumenModMenuIntegration.java delete mode 100644 addon/mc_mod/src/client/java/com/robcholz/lumen/client/LumenSerialManager.java delete mode 100644 addon/mc_mod/src/client/java/com/robcholz/lumen/client/SerialPortLocator.java delete mode 100644 addon/mc_mod/src/client/java/com/robcholz/lumen/client/config/LumenConfig.java delete mode 100644 addon/mc_mod/src/client/java/com/robcholz/lumen/client/config/LumenConfigManager.java delete mode 100644 addon/mc_mod/src/client/java/com/robcholz/lumen/client/config/LumenConfigScreen.java delete mode 100644 addon/mc_mod/src/main/resources/assets/lumen/icon.png delete mode 100644 addon/mc_mod/src/main/resources/fabric.mod.json diff --git a/addon/mc_mod/.gitignore b/addon/mc_mod/.gitignore deleted file mode 100644 index d5f737e..0000000 --- a/addon/mc_mod/.gitignore +++ /dev/null @@ -1,119 +0,0 @@ -# User-specific stuff -.idea/ - -*.iml -*.ipr -*.iws - -# IntelliJ -out/ -# mpeltonen/sbt-idea plugin -.idea_modules/ - -# JIRA plugin -atlassian-ide-plugin.xml - -# Compiled class file -*.class - -# Log file -*.log - -# BlueJ files -*.ctxt - -# Package Files # -*.jar -*.war -*.nar -*.ear -*.zip -*.tar.gz -*.rar - -# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml -hs_err_pid* - -*~ - -# temporary files which can be created if a process still has a handle open of a deleted file -.fuse_hidden* - -# KDE directory preferences -.directory - -# Linux trash folder which might appear on any partition or disk -.Trash-* - -# .nfs files are created when an open file is removed but is still being accessed -.nfs* - -# General -.DS_Store -.AppleDouble -.LSOverride - -# Icon must end with two \r -Icon - -# Thumbnails -._* - -# Files that might appear in the root of a volume -.DocumentRevisions-V100 -.fseventsd -.Spotlight-V100 -.TemporaryItems -.Trashes -.VolumeIcon.icns -.com.apple.timemachine.donotpresent - -# Directories potentially created on remote AFP share -.AppleDB -.AppleDesktop -Network Trash Folder -Temporary Items -.apdisk - -# Windows thumbnail cache files -Thumbs.db -Thumbs.db:encryptable -ehthumbs.db -ehthumbs_vista.db - -# Dump file -*.stackdump - -# Folder config file -[Dd]esktop.ini - -# Recycle Bin used on file shares -$RECYCLE.BIN/ - -# Windows Installer files -*.cab -*.msi -*.msix -*.msm -*.msp - -# Windows shortcuts -*.lnk - -.gradle -build/ - -# Ignore Gradle GUI config -gradle-app.setting - -# Cache of project -.gradletasknamecache - -**/build/ - -# Common working directory -run/ -runs/ - -# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) -!gradle-wrapper.jar diff --git a/addon/mc_mod/build.gradle b/addon/mc_mod/build.gradle deleted file mode 100644 index 177dd69..0000000 --- a/addon/mc_mod/build.gradle +++ /dev/null @@ -1,93 +0,0 @@ -plugins { - id 'fabric-loom' version '1.13.6' - id 'maven-publish' -} - -version = project.mod_version -group = project.maven_group - -base { - archivesName = "${project.archives_base_name}+${project.fabric_version}" -} - -repositories { - maven { - name = "Terraformers" - url = "https://maven.terraformersmc.com/" - } -} - -loom { - splitEnvironmentSourceSets() - - mods { - "lumen" { - sourceSet sourceSets.main - sourceSet sourceSets.client - } - } - -} - -dependencies { - // To change the versions see the gradle.properties file - minecraft "com.mojang:minecraft:${project.minecraft_version}" - mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2" - modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" - - modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}" - modImplementation "com.terraformersmc:modmenu:${project.modmenu_version}" - implementation "com.fazecast:jSerialComm:2.11.0" -} - -processResources { - inputs.property "version", project.version - inputs.property "minecraft_version", project.minecraft_version - inputs.property "loader_version", project.loader_version - - filesMatching("fabric.mod.json") { - expand "version": project.version, - "minecraft_version": project.minecraft_version, - "loader_version": project.loader_version - } -} - -tasks.withType(JavaCompile).configureEach { - it.options.release = 21 -} - -java { - // Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task - // if it is present. - // If you remove this line, sources will not be generated. - withSourcesJar() - - sourceCompatibility = JavaVersion.VERSION_21 - targetCompatibility = JavaVersion.VERSION_21 -} - -jar { - inputs.property "archivesName", project.base.archivesName - - from("LICENSE") { - rename { "${it}_${inputs.properties.archivesName}" } - } -} - -// configure the maven publication -publishing { - publications { - create("mavenJava", MavenPublication) { - artifactId = project.archives_base_name - from components.java - } - } - - // See https://docs.gradle.org/current/userguide/publishing_maven.html for information on how to set up publishing. - repositories { - // Add repositories to publish to here. - // Notice: This block does NOT have the same function as the block in the top level. - // The repositories here will be used for publishing your artifact, not for - // retrieving dependencies. - } -} diff --git a/addon/mc_mod/gradle.properties b/addon/mc_mod/gradle.properties deleted file mode 100644 index fa81590..0000000 --- a/addon/mc_mod/gradle.properties +++ /dev/null @@ -1,16 +0,0 @@ -# Done to increase the memory available to gradle. -org.gradle.jvmargs=-Xmx1G -# Fabric Properties -# check these on https://modmuss50.me/fabric.html -minecraft_version=1.21.3 -yarn_mappings=1.21.3+build.2 -loader_version=0.17.2 -loom_version=1.12 -# Mod Properties -mod_version=1.0.0 -maven_group=com.robcholz -archives_base_name=lumen -# Dependencies -# check this on https://modmuss50.me/fabric.html -fabric_version=0.112.1+1.21.3 -modmenu_version=12.0.0 diff --git a/addon/mc_mod/gradle/wrapper/gradle-wrapper.jar b/addon/mc_mod/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index f8e1ee3125fe0768e9a76ee977ac089eb657005e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 45633 zcma&NV|1n6wyqu9PQ|uu+csuwn-$x(T~Woh?Nr6KUD3(A)@l1Yd+oj6Z_U=8`RAE` z#vE6_`?!1WLs1443=Ieh3JM4ai0JG2|2{}S&_HrxszP*9^5P7#QX*pVDq?D?;6T8C z{bWO1$9at%!*8ax*TT&F99vwf1Ls+3lklsb|bC`H`~Q z_w}*E9P=Wq;PYlGYhZ^lt#N97bt5aZ#mQcOr~h^B;R>f-b0gf{y(;VA{noAt`RZzU z7vQWD{%|q!urW2j0Z&%ChtL(^9m` zgaU%|B;V#N_?%iPvu0PVkX=1m9=*SEGt-Lp#&Jh%rz6EJXlV^O5B5YfM5j{PCeElx z8sipzw8d=wVhFK+@mgrWyA)Sv3BJq=+q+cL@=wuH$2;LjY z^{&+X4*HFA0{QvlM_V4PTQjIdd;d|2YuN;s|bi!@<)r-G%TuOCHz$O(_-K z)5in&6uNN<0UfwY=K>d;cL{{WK2FR|NihJMN0Q4X+(1lE)$kY?T$7UWleIU`i zQG#X-&&m-8x^(;n@o}$@vPMYRoq~|FqC~CU3MnoiifD{(CwAGd%X#kFHq#4~%_a!{ zeX{XXDT#(DvX7NtAs7S}2ZuiZ>gtd;tCR7E)3{J^`~#Vd**9qz%~JRFAiZf{zt|Dr zvQw!)n7fNUn_gH`o9?8W8t_%x6~=y*`r46bjj(t{YU*qfqd}J}*mkgUfsXTI>Uxl6 z)Fj>#RMy{`wINIR;{_-!xGLgVaTfNJ2-)%YUfO&X5z&3^E#4?k-_|Yv$`fpgYkvnA%E{CiV zP|-zAf8+1@R`sT{rSE#)-nuU7Pwr-z>0_+CLQT|3vc-R22ExKT4ym@Gj77j$aTVns zp4Kri#Ml?t7*n(;>nkxKdhOU9Qbwz%*#i9_%K<`m4T{3aPbQ?J(Mo`6E5cDdbAk%X z+4bN%E#a(&ZXe{G#V!2Nt+^L$msKVHP z|APpBhq7knz(O2yY)$$VyI_Xg4UIC*$!i7qQG~KEZnO@Q1i89@4ZKW*3^Wh?o?zSkfPxdhnTxlO!3tAqe_ zuEqHVcAk3uQIFTpP~C{d$?>7yt3G3Fo>syXTus>o0tJdFpQWC27hDiwC%O09i|xCq z@H6l|+maB;%CYQIChyhu;PVYz9e&5a@EEQs3$DS6dLIS+;N@I0)V}%B`jdYv;JDck zd|xxp(I?aedivE7*19hesoa-@Xm$^EHbbVmh$2^W-&aTejsyc$i+}A#n2W*&0Qt`5 zJS!2A|LVV;L!(*x2N)GjJC;b1RB_f(#D&g_-};a*|BTRvfdIX}Gau<;uCylMNC;UG zzL((>6KQBQ01wr%7u9qI2HLEDY!>XisIKb#6=F?pAz)!_JX}w|>1V>X^QkMdFi@Jr z`1N*V4xUl{qvECHoF?#lXuO#Dg2#gh|AU$Wc=nuIbmVPBEGd(R#&Z`TP9*o%?%#ob zWN%ByU+55yBNfjMjkJnBjT!cVDi}+PR3N&H(f8$d^Pu;A_WV*{)c2Q{IiE7&LPsd4 z!rvkUf{sco_WNSIdW+btM#O+4n`JiceH6%`7pDV zRqJ@lj=Dt(e-Gkz$b!c2>b)H$lf(fuAPdIsLSe(dZ4E~9+Ge!{3j~>nS%r)eQZ;Iq ztWGpp=2Ptc!LK_TQ8cgJXUlU5mRu|7F2{eu*;a>_5S<;bus=t*IXcfzJRPv4xIs;s zt2<&}OM>KxkTxa=dFMfNr42=DL~I}6+_{`HT_YJBiWkpVZND1Diad~Yr*Fuq{zljr z*_+jXk=qVBdwlQkYuIrB4GG*#voba$?h*u0uRNL+87-?AjzG2X_R9mzQ7BJEawutObr|ey~%in>6k%A`K*`pb-|DF5m})!`b=~osoiW2)IFh?_y9y<3Cix_ znvC=bjBX1J820!%%9FaB@v?hAsd05e@w$^ZAvtUp*=Bi+Owkl?rLa6F#yl{s+?563 zmn2 zV95%gySAJ$L!Vvk4kx!n@mo`3Mfi`2lXUkBmd%)u)7C?Pa;oK~zUQ#p0u{a|&0;zNO#9a4`v^3df90X#~l_k$q7n&L5 z?TszF842~g+}tgUP}UG?ObLCE1(Js_$e>XS7m%o7j@@VdxePtg)w{i5an+xK95r?s zDeEhgMO-2$H?@0{p-!4NJ)}zP+3LzZB?FVap)ObHV6wp}Lrxvz$cjBND1T6ln$EfJ zZRPeR2lP}K0p8x`ahxB??Ud;i7$Y5X!5}qBFS+Zp=P^#)08nQi_HuJcN$0=x;2s53 zwoH}He9BlKT4GdWfWt)@o@$4zN$B@5gVIN~aHtwIhh{O$uHiMgYl=&Vd$w#B2 zRv+xK3>4E{!)+LXA2#*K6H~HpovXAQeXV(^Pd%G_>ro0(4_@`{2Ag(+8{9pqJ>Co$ zRRV(oX;nD+Jel_2^BlNO=cQP8q*G#~R3PTERUxvug_C4T3qwb9MQE|^{5(H*nt`fn z^%*p-RwkAhT6(r>E@5w8FaB)Q<{#`H9fTdc6QBuSr9D-x!Tb9f?wI=M{^$cB5@1;0 z+yLHh?3^c-Qte@JI<SW`$bs5Vv9!yWjJD%oY z8Cdc$a(LLy@tB2)+rUCt&0$&+;&?f~W6+3Xk3g zy9L�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 diff --git a/addon/mc_mod/gradle/wrapper/gradle-wrapper.properties b/addon/mc_mod/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 23449a2..0000000 --- a/addon/mc_mod/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,7 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip -networkTimeout=10000 -validateDistributionUrl=true -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/addon/mc_mod/gradlew b/addon/mc_mod/gradlew deleted file mode 100755 index adff685..0000000 --- a/addon/mc_mod/gradlew +++ /dev/null @@ -1,248 +0,0 @@ -#!/bin/sh - -# -# Copyright © 2015 the original authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# SPDX-License-Identifier: Apache-2.0 -# - -############################################################################## -# -# Gradle start up script for POSIX generated by Gradle. -# -# Important for running: -# -# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is -# noncompliant, but you have some other compliant shell such as ksh or -# bash, then to run this script, type that shell name before the whole -# command line, like: -# -# ksh Gradle -# -# Busybox and similar reduced shells will NOT work, because this script -# requires all of these POSIX shell features: -# * functions; -# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», -# «${var#prefix}», «${var%suffix}», and «$( cmd )»; -# * compound commands having a testable exit status, especially «case»; -# * various built-in commands including «command», «set», and «ulimit». -# -# Important for patching: -# -# (2) This script targets any POSIX shell, so it avoids extensions provided -# by Bash, Ksh, etc; in particular arrays are avoided. -# -# The "traditional" practice of packing multiple parameters into a -# space-separated string is a well documented source of bugs and security -# problems, so this is (mostly) avoided, by progressively accumulating -# options in "$@", and eventually passing that to Java. -# -# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, -# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; -# see the in-line comments for details. -# -# There are tweaks for specific operating systems such as AIX, CygWin, -# Darwin, MinGW, and NonStop. -# -# (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt -# within the Gradle project. -# -# You can find Gradle at https://github.com/gradle/gradle/. -# -############################################################################## - -# Attempt to set APP_HOME - -# Resolve links: $0 may be a link -app_path=$0 - -# Need this for daisy-chained symlinks. -while - APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path - [ -h "$app_path" ] -do - ls=$( ls -ld "$app_path" ) - link=${ls#*' -> '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac -done - -# This is normally unused -# shellcheck disable=SC2034 -APP_BASE_NAME=${0##*/} -# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD=maximum - -warn () { - echo "$*" -} >&2 - -die () { - echo - echo "$*" - echo - exit 1 -} >&2 - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; -esac - - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java - else - JAVACMD=$JAVA_HOME/bin/java - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD=java - if ! command -v java >/dev/null 2>&1 - then - die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -fi - -# Increase the maximum file descriptors if we can. -if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac -fi - -# Collect all arguments for the java command, stacking in reverse order: -# * args from the command line -# * the main class name -# * -classpath -# * -D...appname settings -# * --module-path (only if needed) -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. - -# For Cygwin or MSYS, switch paths to Windows format before running java -if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) - fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg - done -fi - - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, -# and any embedded shellness will be escaped. -# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be -# treated as '${Hostname}' itself on the command line. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ - "$@" - -# Stop when "xargs" is not available. -if ! command -v xargs >/dev/null 2>&1 -then - die "xargs is not available" -fi - -# Use "xargs" to parse quoted args. -# -# With -n1 it outputs one arg per line, with the quotes and backslashes removed. -# -# In Bash we could simply go: -# -# readarray ARGS < <( xargs -n1 <<<"$var" ) && -# set -- "${ARGS[@]}" "$@" -# -# but POSIX shell has neither arrays nor command substitution, so instead we -# post-process each arg (as a line of input to sed) to backslash-escape any -# character that might be a shell metacharacter, then use eval to reverse -# that process (while maintaining the separation between arguments), and wrap -# the whole thing up as a single "set" statement. -# -# This will of course break if any of these variables contains a newline or -# an unmatched quote. -# - -eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' - -exec "$JAVACMD" "$@" diff --git a/addon/mc_mod/gradlew.bat b/addon/mc_mod/gradlew.bat deleted file mode 100644 index c4bdd3a..0000000 --- a/addon/mc_mod/gradlew.bat +++ /dev/null @@ -1,93 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem -@rem SPDX-License-Identifier: Apache-2.0 -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:execute -@rem Setup the command line - - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/addon/mc_mod/settings.gradle b/addon/mc_mod/settings.gradle deleted file mode 100644 index f91a4fe..0000000 --- a/addon/mc_mod/settings.gradle +++ /dev/null @@ -1,9 +0,0 @@ -pluginManagement { - repositories { - maven { - name = 'Fabric' - url = 'https://maven.fabricmc.net/' - } - gradlePluginPortal() - } -} diff --git a/addon/mc_mod/src/client/java/com/robcholz/lumen/LumenSyncState.java b/addon/mc_mod/src/client/java/com/robcholz/lumen/LumenSyncState.java deleted file mode 100644 index b9c207c..0000000 --- a/addon/mc_mod/src/client/java/com/robcholz/lumen/LumenSyncState.java +++ /dev/null @@ -1,303 +0,0 @@ -package com.robcholz.lumen; - -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.network.AbstractClientPlayerEntity; -import net.minecraft.client.texture.AbstractTexture; -import net.minecraft.client.texture.NativeImage; -import net.minecraft.client.texture.ResourceTexture; -import net.minecraft.client.util.SkinTextures; -import net.minecraft.entity.player.PlayerEntity; -import net.minecraft.util.Identifier; -import net.minecraft.world.GameMode; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.Optional; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; - -public final class LumenSyncState { - private static final Logger LOGGER = LoggerFactory.getLogger(LumenSyncState.class); - private static final int SKIN_HEIGHT = 120; - - private LumenSyncState() { - } - - public static Snapshot requestPlayerSnapshot() { - MinecraftClient client = MinecraftClient.getInstance(); - if (client == null) { - return Snapshot.defaultSnapshot(); - } - CompletableFuture future = new CompletableFuture<>(); - client.execute(() -> future.complete(capturePlayerSnapshot(client))); - try { - return future.get(2, TimeUnit.SECONDS); - } catch (Exception e) { - LOGGER.debug("Failed to capture snapshot", e); - return Snapshot.defaultSnapshot(); - } - } - - public static SkinPayload requestSkinPayload() { - MinecraftClient client = MinecraftClient.getInstance(); - if (client == null) { - return SkinPayload.empty(); - } - CompletableFuture future = new CompletableFuture<>(); - client.execute(() -> future.complete(captureSkinPayload(client))); - try { - return future.get(5, TimeUnit.SECONDS); - } catch (Exception e) { - LOGGER.debug("Failed to capture skin payload", e); - return SkinPayload.empty(); - } - } - - private static Snapshot capturePlayerSnapshot(MinecraftClient client) { - if (client.player == null) { - return Snapshot.defaultSnapshot(); - } - - PlayerEntity player = client.player; - String mode = resolveMode(client); - double health = player.getHealth(); - double maxHealth = player.getMaxHealth(); - return new Snapshot(mode, health, maxHealth); - } - - private static SkinPayload captureSkinPayload(MinecraftClient client) { - if (client.player == null) { - return SkinPayload.empty(); - } - return readSkin(client, client.player); - } - - private static String resolveMode(MinecraftClient client) { - if (client.interactionManager == null) { - return "-----"; - } - GameMode mode = client.interactionManager.getCurrentGameMode(); - if (mode == null) { - return "-----"; - } - return switch (mode) { - case CREATIVE -> "Creative"; - case ADVENTURE -> "Adventure"; - case SPECTATOR -> "Spectator"; - default -> "Survival"; - }; - } - - private static byte[] toRGB565(NativeImage image) { - int width = image.getWidth(); - int height = image.getHeight(); - byte[] data = new byte[width * height * 2]; - int idx = 0; - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - int color = image.getColorArgb(x, y); - int r = (color >>> 16) & 0xFF; - int g = (color >>> 8) & 0xFF; - int b = color & 0xFF; - int value = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >>> 3); - data[idx++] = (byte) (value & 0xFF); - data[idx++] = (byte) ((value >>> 8) & 0xFF); // little-endian - } - } - return data; - } - - private static NativeImage scaleImage(NativeImage image) { - int srcWidth = image.getWidth(); - int srcHeight = image.getHeight(); - int dstHeight = SKIN_HEIGHT; - int dstWidth = Math.max(1, Math.round((srcWidth * (float) dstHeight) / srcHeight)); - NativeImage scaled = new NativeImage(dstWidth, dstHeight, true); - for (int y = 0; y < dstHeight; y++) { - int srcY = (y * srcHeight) / dstHeight; - for (int x = 0; x < dstWidth; x++) { - int srcX = (x * srcWidth) / dstWidth; - int color = image.getColorArgb(srcX, srcY); - scaled.setColorArgb(x, y, color); - } - } - return scaled; - } - - private static SkinPayload readSkin(MinecraftClient client, PlayerEntity player) { - if (!(player instanceof AbstractClientPlayerEntity clientPlayer)) { - return SkinPayload.empty(); - } - SkinTextures textures = clientPlayer.getSkinTextures(); - if (textures == null || textures.texture() == null) { - return SkinPayload.empty(); - } - AbstractTexture texture = client.getTextureManager().getTexture(textures.texture()); - if (texture instanceof ResourceTexture resourceTexture) { - Optional image = encodeResourceTexture(client, resourceTexture); - if (image.isEmpty()) { - return SkinPayload.empty(); - } - NativeImage frontView = image.get(); - NativeImage scaled = scaleImage(frontView); - byte[] bytes = toRGB565(scaled); - return new SkinPayload(scaled.getWidth(), scaled.getHeight(), bytes); - } - return SkinPayload.empty(); - } - - private static Optional encodeFrontView(NativeImage skin) { - NativeImage front = buildFrontView(skin); - return Optional.of(front); - } - - private static Optional encodeResourceTexture(MinecraftClient client, ResourceTexture texture) { - Identifier location = getResourceTextureLocation(texture); - if (location == null) { - return Optional.empty(); - } - Object resourceManager = client.getResourceManager(); - Object textureData = null; - try { - Class resourceManagerClass = Class.forName("net.minecraft.resource.ResourceManager"); - Class textureDataClass = Class.forName("net.minecraft.client.texture.ResourceTexture$TextureData"); - var load = textureDataClass.getDeclaredMethod("load", resourceManagerClass, Identifier.class); - load.setAccessible(true); - textureData = load.invoke(null, resourceManager, location); - var getImage = textureDataClass.getDeclaredMethod("getImage"); - getImage.setAccessible(true); - NativeImage image = (NativeImage) getImage.invoke(textureData); - return encodeFrontView(image); - } catch (Exception e) { - LOGGER.debug("Failed to read resource texture {}", location, e); - return Optional.empty(); - } finally { - if (textureData != null) { - try { - var close = textureData.getClass().getDeclaredMethod("close"); - close.setAccessible(true); - close.invoke(textureData); - } catch (Exception e) { - LOGGER.debug("Failed to close resource texture data", e); - } - } - } - } - - private static Identifier getResourceTextureLocation(ResourceTexture texture) { - try { - var field = ResourceTexture.class.getDeclaredField("location"); - field.setAccessible(true); - return (Identifier) field.get(texture); - } catch (ReflectiveOperationException e) { - LOGGER.debug("Failed to access ResourceTexture location", e); - return null; - } - } - - private static NativeImage buildFrontView(NativeImage skin) { - int skinWidth = skin.getWidth(); - int skinHeight = skin.getHeight(); - boolean hasSecondLayer = skinHeight >= 64 && skinWidth >= 64; - NativeImage front = new NativeImage(16, 32, true); - - // Head - blit(skin, 8, 8, 8, 8, front, 4, 0); - if (hasSecondLayer) { - blitAlpha(skin, 40, 8, 8, 8, front, 4, 0); - } - - // Body - blit(skin, 20, 20, 8, 12, front, 4, 8); - if (hasSecondLayer) { - blitAlpha(skin, 20, 36, 8, 12, front, 4, 8); - } - - // Right arm - blit(skin, 44, 20, 4, 12, front, 0, 8); - if (hasSecondLayer) { - blitAlpha(skin, 44, 36, 4, 12, front, 0, 8); - } - - // Left arm - if (hasSecondLayer) { - blit(skin, 36, 52, 4, 12, front, 12, 8); - blitAlpha(skin, 52, 52, 4, 12, front, 12, 8); - } else { - blit(skin, 44, 20, 4, 12, front, 12, 8); - } - - // Right leg - blit(skin, 4, 20, 4, 12, front, 4, 20); - if (hasSecondLayer) { - blitAlpha(skin, 4, 36, 4, 12, front, 4, 20); - } - - // Left leg - if (hasSecondLayer) { - blit(skin, 20, 52, 4, 12, front, 8, 20); - blitAlpha(skin, 4, 52, 4, 12, front, 8, 20); - } else { - blit(skin, 4, 20, 4, 12, front, 8, 20); - } - - return front; - } - - private static void blit(NativeImage src, int sx, int sy, int w, int h, NativeImage dst, int dx, int dy) { - for (int y = 0; y < h; y++) { - for (int x = 0; x < w; x++) { - int color = src.getColorArgb(sx + x, sy + y); - dst.setColorArgb(dx + x, dy + y, color); - } - } - } - - private static void blitAlpha(NativeImage src, int sx, int sy, int w, int h, NativeImage dst, int dx, int dy) { - for (int y = 0; y < h; y++) { - for (int x = 0; x < w; x++) { - int color = src.getColorArgb(sx + x, sy + y); - int alpha = (color >>> 24) & 0xFF; - if (alpha == 0) { - continue; - } - dst.setColorArgb(dx + x, dy + y, color); - } - } - } - - public record Snapshot( - String mode, - double health, - double maxHealth - ) { - public static Snapshot defaultSnapshot() { - return new Snapshot("-----", 0.0, 0.0); - } - } - - public record SkinPayload(int width, int height, byte[] rgb565) { - public static SkinPayload empty() { - return new SkinPayload(0, 0, new byte[0]); - } - - public byte[] toWireBytes() { - if (rgb565.length == 0) { - return new byte[0]; - } - byte[] data = new byte[4 + rgb565.length]; - data[0] = (byte) (width & 0xFF); - data[1] = (byte) ((width >>> 8) & 0xFF); - data[2] = (byte) (height & 0xFF); - data[3] = (byte) ((height >>> 8) & 0xFF); - - for (int i = 0; i < rgb565.length; i += 2) { - byte lsb = rgb565[i]; - byte msb = rgb565[i + 1]; - data[4 + i] = msb; - data[4 + i + 1] = lsb; - } - return data; - } - } -} diff --git a/addon/mc_mod/src/client/java/com/robcholz/lumen/SerialPackClient.java b/addon/mc_mod/src/client/java/com/robcholz/lumen/SerialPackClient.java deleted file mode 100644 index 1dab37a..0000000 --- a/addon/mc_mod/src/client/java/com/robcholz/lumen/SerialPackClient.java +++ /dev/null @@ -1,70 +0,0 @@ -package com.robcholz.lumen; - -import com.fazecast.jSerialComm.SerialPort; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; - -public class SerialPackClient implements AutoCloseable { - private final SerialPort port; - private final String portPath; - - public SerialPackClient(String portPath) throws IOException { - this.portPath = portPath; - port = SerialPort.getCommPort(portPath); - if (port == null) { - throw new IOException("Serial port not found: " + portPath); - } - port.setBaudRate(460800); - port.setComPortTimeouts(SerialPort.TIMEOUT_WRITE_BLOCKING, 0, 0); - if (!port.openPort()) { - throw new IOException("Failed to open serial port: " + portPath); - } - } - - private static byte[] encodeU32(int value) { - return new byte[]{ - (byte) (value & 0xFF), - (byte) ((value >>> 8) & 0xFF), - (byte) ((value >>> 16) & 0xFF), - (byte) ((value >>> 24) & 0xFF) - }; - } - - public synchronized void send(String path, byte[] data) throws IOException { - if (!port.isOpen()) { - throw new IOException("Serial port is closed"); - } - byte[] pathBytes = path.getBytes(StandardCharsets.UTF_8); - writeAll(pathBytes); - writeAll(new byte[]{'\n'}); - writeAll(encodeU32(data.length)); - writeAll(data); - } - - public boolean isOpen() { - return port.isOpen(); - } - - public String getPortPath() { - return portPath; - } - - private void writeAll(byte[] bytes) throws IOException { - int offset = 0; - while (offset < bytes.length) { - int written = port.writeBytes(bytes, bytes.length - offset, offset); - if (written <= 0) { - throw new IOException("Serial write failed"); - } - offset += written; - } - } - - @Override - public void close() { - if (port.isOpen()) { - port.closePort(); - } - } -} diff --git a/addon/mc_mod/src/client/java/com/robcholz/lumen/client/LumenClient.java b/addon/mc_mod/src/client/java/com/robcholz/lumen/client/LumenClient.java deleted file mode 100644 index 01dce39..0000000 --- a/addon/mc_mod/src/client/java/com/robcholz/lumen/client/LumenClient.java +++ /dev/null @@ -1,73 +0,0 @@ -package com.robcholz.lumen.client; - -import com.robcholz.lumen.LumenSyncState; -import com.robcholz.lumen.client.config.LumenConfig; -import com.robcholz.lumen.client.config.LumenConfigManager; -import net.fabricmc.api.ClientModInitializer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.nio.charset.StandardCharsets; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; - -public class LumenClient implements ClientModInitializer { - public static final String MOD_ID = "lumen"; - public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); - private static final ScheduledExecutorService SERIAL_EXECUTOR = - Executors.newSingleThreadScheduledExecutor(r -> { - Thread thread = new Thread(r, "lumen-serial-sync"); - thread.setDaemon(true); - return thread; - }); - - private static void sendPlayerInfo(LumenSerialManager serial) { - try { - LumenSyncState.Snapshot snapshot = LumenSyncState.requestPlayerSnapshot(); - String json = "{\"mode\":\"" + snapshot.mode() + "\",\"health\":" + - snapshot.health() + ",\"max_health\":" + snapshot.maxHealth() + "}"; - serial.send("sync", json.getBytes(StandardCharsets.UTF_8)); - } catch (Exception e) { - LOGGER.debug("Failed to send player info over serial", e); - } - } - - private static void sendSkinInfo(LumenSerialManager serial) { - try { - LumenSyncState.SkinPayload payload = LumenSyncState.requestSkinPayload(); - byte[] data = payload.toWireBytes(); - if (data.length == 0) { - return; - } - serial.send("sync/skin", data); - } catch (Exception e) { - LOGGER.debug("Failed to send skin info over serial", e); - } - } - - @Override - public void onInitializeClient() { - LumenConfig config = LumenConfigManager.get(); - String portPath = SerialPortLocator.resolvePortPath(config); - if (portPath == null || portPath.isBlank()) { - LOGGER.warn("No serial port configured; set it in Mod Menu or via -Dlumen.serialPort/LUMEN_SERIAL_PORT"); - } - - LumenSerialManager serial = new LumenSerialManager(); - SERIAL_EXECUTOR.scheduleAtFixedRate( - () -> sendPlayerInfo(serial), - 0, - 1, - TimeUnit.SECONDS - ); - SERIAL_EXECUTOR.scheduleAtFixedRate( - () -> sendSkinInfo(serial), - 0, - 20, - TimeUnit.SECONDS - ); - - LOGGER.info("Lumen client ready"); - } -} diff --git a/addon/mc_mod/src/client/java/com/robcholz/lumen/client/LumenModMenuIntegration.java b/addon/mc_mod/src/client/java/com/robcholz/lumen/client/LumenModMenuIntegration.java deleted file mode 100644 index d612746..0000000 --- a/addon/mc_mod/src/client/java/com/robcholz/lumen/client/LumenModMenuIntegration.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.robcholz.lumen.client; - -import com.robcholz.lumen.client.config.LumenConfigScreen; -import com.terraformersmc.modmenu.api.ConfigScreenFactory; -import com.terraformersmc.modmenu.api.ModMenuApi; - -public class LumenModMenuIntegration implements ModMenuApi { - @Override - public ConfigScreenFactory getModConfigScreenFactory() { - return LumenConfigScreen::new; - } -} diff --git a/addon/mc_mod/src/client/java/com/robcholz/lumen/client/LumenSerialManager.java b/addon/mc_mod/src/client/java/com/robcholz/lumen/client/LumenSerialManager.java deleted file mode 100644 index 4675b32..0000000 --- a/addon/mc_mod/src/client/java/com/robcholz/lumen/client/LumenSerialManager.java +++ /dev/null @@ -1,83 +0,0 @@ -package com.robcholz.lumen.client; - -import com.robcholz.lumen.SerialPackClient; -import com.robcholz.lumen.client.config.LumenConfig; -import com.robcholz.lumen.client.config.LumenConfigManager; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; - -public final class LumenSerialManager implements AutoCloseable { - private static final Logger LOGGER = LoggerFactory.getLogger("lumen"); - - private SerialPackClient client; - private String connectedPortPath; - private long lastReconnectAttemptMillis; - private boolean hasAttemptedInitialConnect; - - public void send(String path, byte[] data) { - if (!ensureConnected()) { - return; - } - try { - client.send(path, data); - } catch (IOException e) { - LOGGER.debug("Serial send failed, closing connection", e); - closeClient(); - } - } - - private boolean ensureConnected() { - LumenConfig config = LumenConfigManager.get(); - String desiredPortPath = SerialPortLocator.resolvePortPath(config); - if (desiredPortPath == null || desiredPortPath.isBlank()) { - return false; - } - if (client != null && !client.isOpen()) { - closeClient(); - } - if (client != null && !desiredPortPath.equals(connectedPortPath)) { - closeClient(); - } - if (client != null) { - return true; - } - - long now = System.currentTimeMillis(); - if (hasAttemptedInitialConnect) { - if (!config.autoReconnect) { - return false; - } - long intervalMillis = Math.max(1, config.reconnectPeriodSeconds) * 1000L; - if (now - lastReconnectAttemptMillis < intervalMillis) { - return false; - } - } - - hasAttemptedInitialConnect = true; - lastReconnectAttemptMillis = now; - try { - client = new SerialPackClient(desiredPortPath); - connectedPortPath = desiredPortPath; - return true; - } catch (IOException e) { - LOGGER.debug("Failed to connect to serial port {}", desiredPortPath, e); - closeClient(); - return false; - } - } - - private void closeClient() { - if (client != null) { - client.close(); - } - client = null; - connectedPortPath = null; - } - - @Override - public void close() { - closeClient(); - } -} diff --git a/addon/mc_mod/src/client/java/com/robcholz/lumen/client/SerialPortLocator.java b/addon/mc_mod/src/client/java/com/robcholz/lumen/client/SerialPortLocator.java deleted file mode 100644 index b2bd15c..0000000 --- a/addon/mc_mod/src/client/java/com/robcholz/lumen/client/SerialPortLocator.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.robcholz.lumen.client; - -import com.fazecast.jSerialComm.SerialPort; -import com.robcholz.lumen.client.config.LumenConfig; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -public final class SerialPortLocator { - private SerialPortLocator() { - } - - public static String resolvePortPath(LumenConfig config) { - String configured = normalize(config.portPath); - if (!configured.isEmpty()) { - return configured; - } - String property = normalize(System.getProperty("lumen.serialPort")); - if (!property.isEmpty()) { - return property; - } - String env = normalize(System.getenv("LUMEN_SERIAL_PORT")); - if (!env.isEmpty()) { - return env; - } - return autoDetectPort(); - } - - public static String autoDetectPort() { - SerialPort[] ports = SerialPort.getCommPorts(); - if (ports.length == 0) { - return null; - } - String best = null; - for (SerialPort port : ports) { - String name = normalize(port.getSystemPortName()); - String lower = name.toLowerCase(); - if (lower.contains("usb") || lower.contains("modem") || lower.contains("tty")) { - best = port.getSystemPortName(); - break; - } - } - if (best != null) { - return best; - } - return ports[0].getSystemPortName(); - } - - public static List listPorts() { - SerialPort[] ports = SerialPort.getCommPorts(); - List names = new ArrayList<>(ports.length); - for (SerialPort port : ports) { - String name = normalize(port.getSystemPortName()); - if (!name.isEmpty()) { - names.add(name); - } - } - Collections.sort(names); - return names; - } - - private static String normalize(String value) { - if (value == null) { - return ""; - } - return value.trim(); - } -} diff --git a/addon/mc_mod/src/client/java/com/robcholz/lumen/client/config/LumenConfig.java b/addon/mc_mod/src/client/java/com/robcholz/lumen/client/config/LumenConfig.java deleted file mode 100644 index 0ba7ffe..0000000 --- a/addon/mc_mod/src/client/java/com/robcholz/lumen/client/config/LumenConfig.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.robcholz.lumen.client.config; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.io.Reader; -import java.io.Writer; -import java.nio.file.Files; -import java.nio.file.Path; - -public class LumenConfig { - private static final Logger LOGGER = LoggerFactory.getLogger("lumen"); - private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); - - public String portPath = ""; - public boolean autoReconnect = true; - public int reconnectPeriodSeconds = 5; - - public static LumenConfig load(Path path) { - if (!Files.exists(path)) { - return new LumenConfig(); - } - try (Reader reader = Files.newBufferedReader(path)) { - LumenConfig config = GSON.fromJson(reader, LumenConfig.class); - if (config == null) { - return new LumenConfig(); - } - config.normalize(); - return config; - } catch (IOException e) { - LOGGER.warn("Failed to load config from {}", path, e); - return new LumenConfig(); - } - } - - public LumenConfig copy() { - LumenConfig copy = new LumenConfig(); - copy.portPath = portPath; - copy.autoReconnect = autoReconnect; - copy.reconnectPeriodSeconds = reconnectPeriodSeconds; - return copy; - } - - public void save(Path path) { - normalize(); - try { - Files.createDirectories(path.getParent()); - try (Writer writer = Files.newBufferedWriter(path)) { - GSON.toJson(this, writer); - } - } catch (IOException e) { - LOGGER.warn("Failed to save config to {}", path, e); - } - } - - private void normalize() { - if (portPath == null) { - portPath = ""; - } - if (reconnectPeriodSeconds < 1) { - reconnectPeriodSeconds = 1; - } - } -} diff --git a/addon/mc_mod/src/client/java/com/robcholz/lumen/client/config/LumenConfigManager.java b/addon/mc_mod/src/client/java/com/robcholz/lumen/client/config/LumenConfigManager.java deleted file mode 100644 index e93f177..0000000 --- a/addon/mc_mod/src/client/java/com/robcholz/lumen/client/config/LumenConfigManager.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.robcholz.lumen.client.config; - -import net.fabricmc.loader.api.FabricLoader; - -import java.nio.file.Path; - -public final class LumenConfigManager { - private static final String FILE_NAME = "lumen.json"; - private static final Path CONFIG_PATH = FabricLoader.getInstance().getConfigDir().resolve(FILE_NAME); - private static volatile LumenConfig config = LumenConfig.load(CONFIG_PATH); - - private LumenConfigManager() { - } - - public static LumenConfig get() { - return config; - } - - public static void save(LumenConfig updated) { - config = updated; - updated.save(CONFIG_PATH); - } -} diff --git a/addon/mc_mod/src/client/java/com/robcholz/lumen/client/config/LumenConfigScreen.java b/addon/mc_mod/src/client/java/com/robcholz/lumen/client/config/LumenConfigScreen.java deleted file mode 100644 index 41e6748..0000000 --- a/addon/mc_mod/src/client/java/com/robcholz/lumen/client/config/LumenConfigScreen.java +++ /dev/null @@ -1,299 +0,0 @@ -package com.robcholz.lumen.client.config; - -import com.robcholz.lumen.client.SerialPortLocator; -import net.minecraft.client.gui.screen.Screen; -import net.minecraft.client.gui.widget.ButtonWidget; -import net.minecraft.client.gui.widget.TextFieldWidget; -import net.minecraft.text.Text; - -import java.util.List; - -public class LumenConfigScreen extends Screen { - private static final int ENTRY_HEIGHT = 20; - private static final int LIST_PADDING = 2; - private final Screen parent; - private final LumenConfig workingConfig; - private TextFieldWidget portField; - private TextFieldWidget reconnectField; - private boolean autoReconnectEnabled; - private ButtonWidget autoReconnectButton; - private ButtonWidget portSelectButton; - private ButtonWidget refreshPortsButton; - private List portOptions = List.of(); - private boolean showPortList; - private String lastPortFieldValue = ""; - private int portListX; - private int portListY; - private int portListWidth; - private int scrollOffset; - - public LumenConfigScreen(Screen parent) { - super(Text.literal("Lumen")); - this.parent = parent; - this.workingConfig = LumenConfigManager.get().copy(); - } - - @Override - protected void init() { - int centerX = width / 2; - int y = height / 4; - - portField = new TextFieldWidget(textRenderer, centerX - 100, y, 200, 20, Text.literal("Port path")); - portField.setMaxLength(256); - portField.setText(workingConfig.portPath); - addSelectableChild(portField); - - y += 26; - portSelectButton = ButtonWidget.builder(portSelectLabel(), button -> { - showPortList = !showPortList; - if (showPortList) { - refreshPortOptions(); - scrollOffset = 0; - } - }).dimensions(centerX - 100, y, 200, 20).build(); - addDrawableChild(portSelectButton); - portListX = centerX - 100; - portListY = y + 22; - portListWidth = 200; - - y += 24; - refreshPortsButton = ButtonWidget.builder(Text.literal("Refresh ports"), button -> { - refreshPortOptions(); - scrollOffset = 0; - }).dimensions(centerX - 100, y, 200, 20).build(); - addDrawableChild(refreshPortsButton); - - y += 24; - addDrawableChild(ButtonWidget.builder(Text.literal("Auto-detect port"), button -> { - String detected = SerialPortLocator.autoDetectPort(); - if (detected != null) { - portField.setText(detected); - } - portSelectButton.setMessage(portSelectLabel()); - }).dimensions(centerX - 100, y, 200, 20).build()); - - y += 30; - autoReconnectEnabled = workingConfig.autoReconnect; - autoReconnectButton = ButtonWidget.builder(autoReconnectLabel(), button -> { - autoReconnectEnabled = !autoReconnectEnabled; - autoReconnectButton.setMessage(autoReconnectLabel()); - }).dimensions(centerX - 100, y, 200, 20).build(); - addDrawableChild(autoReconnectButton); - - y += 26; - reconnectField = new TextFieldWidget(textRenderer, centerX - 100, y, 200, 20, Text.literal("Reconnect period (sec)")); - reconnectField.setMaxLength(4); - reconnectField.setText(Integer.toString(workingConfig.reconnectPeriodSeconds)); - addSelectableChild(reconnectField); - - int buttonY = height - 50; - addDrawableChild(ButtonWidget.builder(Text.literal("Done"), button -> saveAndClose()) - .dimensions(centerX - 100, buttonY, 95, 20) - .build()); - addDrawableChild(ButtonWidget.builder(Text.literal("Cancel"), button -> close()) - .dimensions(centerX + 5, buttonY, 95, 20) - .build()); - - refreshPortOptions(); - setInitialFocus(portField); - } - - @Override - public void close() { - if (client != null) { - client.setScreen(parent); - } - } - - private void saveAndClose() { - String port = portField.getText().trim(); - workingConfig.portPath = port; - workingConfig.autoReconnect = autoReconnectEnabled; - workingConfig.reconnectPeriodSeconds = parseReconnectPeriod(reconnectField.getText()); - LumenConfigManager.save(workingConfig); - close(); - } - - private int parseReconnectPeriod(String text) { - try { - int value = Integer.parseInt(text.trim()); - return Math.max(1, value); - } catch (NumberFormatException e) { - return Math.max(1, workingConfig.reconnectPeriodSeconds); - } - } - - @Override - public void render(net.minecraft.client.gui.DrawContext context, int mouseX, int mouseY, float delta) { - renderBackground(context, mouseX, mouseY, delta); - context.drawCenteredTextWithShadow(textRenderer, Text.literal("Lumen Serial Settings"), width / 2, 20, 0xFFFFFF); - context.drawTextWithShadow(textRenderer, Text.literal("Leave port blank to auto-detect"), width / 2 - 100, height / 4 - 12, 0xAAAAAA); - super.render(context, mouseX, mouseY, delta); - if (showPortList) { - context.getMatrices().push(); - context.getMatrices().translate(0, 0, 400); - renderPortList(context, mouseX, mouseY); - context.getMatrices().pop(); - } - } - - @Override - public void tick() { - super.tick(); - String current = portField.getText(); - if (!current.equals(lastPortFieldValue)) { - lastPortFieldValue = current; - portSelectButton.setMessage(portSelectLabel()); - } - } - - private Text autoReconnectLabel() { - return Text.literal("Auto-reconnect: " + (autoReconnectEnabled ? "On" : "Off")); - } - - private Text portSelectLabel() { - String value = portField.getText().trim(); - if (value.isEmpty()) { - value = "Auto-detect"; - } - return Text.literal("Select port: " + value); - } - - private void refreshPortOptions() { - portOptions = SerialPortLocator.listPorts(); - } - - private void renderPortList(net.minecraft.client.gui.DrawContext context, int mouseX, int mouseY) { - int listSize = totalPortEntries(); - int visibleEntries = Math.min(listSize, maxVisibleEntries()); - int listHeight = visibleEntries * ENTRY_HEIGHT + LIST_PADDING * 2; - int x1 = portListX; - int y1 = portListY; - int x2 = portListX + portListWidth; - int y2 = portListY + listHeight; - - context.fill(x1, y1, x2, y2, 0xFF3A3A3A); - context.fill(x1 + 1, y1 + 1, x2 - 1, y2 - 1, 0xFF111111); - - int startIndex = clampScrollOffset(visibleEntries, listSize); - int endIndex = Math.min(listSize, startIndex + visibleEntries); - for (int index = startIndex; index < endIndex; index++) { - String label = portLabelForIndex(index); - drawPortEntry(context, mouseX, mouseY, index - startIndex, label); - } - - if (listSize > visibleEntries) { - drawScrollBar(context, x2 - 5, y1 + 1, listHeight - 2, startIndex, listSize, visibleEntries); - } - } - - private void drawPortEntry(net.minecraft.client.gui.DrawContext context, int mouseX, int mouseY, int index, String label) { - int x = portListX + LIST_PADDING; - int y = portListY + LIST_PADDING + index * ENTRY_HEIGHT; - int width = portListWidth - LIST_PADDING * 2; - int height = ENTRY_HEIGHT; - boolean hover = mouseX >= x && mouseX <= x + width && mouseY >= y && mouseY <= y + height; - if (hover) { - context.fill(x, y, x + width, y + height, 0xFF3A3A3A); - } - context.drawTextWithShadow(textRenderer, Text.literal(label), x + 4, y + 6, 0xFFFFFF); - } - - @Override - public boolean mouseClicked(double mouseX, double mouseY, int button) { - if (showPortList) { - if (handlePortListClick(mouseX, mouseY)) { - return true; - } - showPortList = false; - return true; - } - return super.mouseClicked(mouseX, mouseY, button); - } - - private boolean handlePortListClick(double mouseX, double mouseY) { - int listSize = totalPortEntries(); - int visibleEntries = Math.min(listSize, maxVisibleEntries()); - int listHeight = visibleEntries * ENTRY_HEIGHT + LIST_PADDING * 2; - int x1 = portListX + LIST_PADDING; - int y1 = portListY + LIST_PADDING; - int x2 = portListX + portListWidth - LIST_PADDING; - int y2 = portListY + listHeight - LIST_PADDING; - if (mouseX < x1 || mouseX > x2 || mouseY < y1 || mouseY > y2) { - return false; - } - int startIndex = clampScrollOffset(visibleEntries, listSize); - int index = (int) ((mouseY - y1) / ENTRY_HEIGHT) + startIndex; - if (index == 0) { - portField.setText(""); - portSelectButton.setMessage(portSelectLabel()); - showPortList = false; - return true; - } - if (portOptions.isEmpty()) { - return true; - } - int portIndex = index - 1; - if (portIndex >= 0 && portIndex < portOptions.size()) { - portField.setText(portOptions.get(portIndex)); - portSelectButton.setMessage(portSelectLabel()); - showPortList = false; - return true; - } - return false; - } - - @Override - public boolean mouseScrolled(double mouseX, double mouseY, double horizontalAmount, double verticalAmount) { - if (showPortList) { - int listSize = totalPortEntries(); - int visibleEntries = Math.min(listSize, maxVisibleEntries()); - if (listSize > visibleEntries) { - int delta = verticalAmount > 0 ? -1 : 1; - scrollOffset = Math.max(0, Math.min(scrollOffset + delta, listSize - visibleEntries)); - return true; - } - } - return super.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount); - } - - private int totalPortEntries() { - return 1 + Math.max(1, portOptions.size()); - } - - private int maxVisibleEntries() { - int maxPixels = Math.max(ENTRY_HEIGHT * 2, height - portListY - 60); - return Math.max(1, maxPixels / ENTRY_HEIGHT); - } - - private int clampScrollOffset(int visibleEntries, int listSize) { - int maxOffset = Math.max(0, listSize - visibleEntries); - if (scrollOffset > maxOffset) { - scrollOffset = maxOffset; - } - return scrollOffset; - } - - private String portLabelForIndex(int index) { - if (index == 0) { - return "Auto-detect"; - } - if (portOptions.isEmpty()) { - return "No ports found"; - } - int portIndex = index - 1; - if (portIndex >= 0 && portIndex < portOptions.size()) { - return portOptions.get(portIndex); - } - return ""; - } - - private void drawScrollBar(net.minecraft.client.gui.DrawContext context, int x, int y, int height, int startIndex, int listSize, int visibleEntries) { - int trackHeight = height; - int thumbHeight = Math.max(12, (trackHeight * visibleEntries) / listSize); - int maxOffset = Math.max(1, listSize - visibleEntries); - int thumbY = y + (trackHeight - thumbHeight) * startIndex / maxOffset; - context.fill(x, y, x + 3, y + height, 0xFF202020); - context.fill(x, thumbY, x + 3, thumbY + thumbHeight, 0xFF8A8A8A); - } -} diff --git a/addon/mc_mod/src/main/resources/assets/lumen/icon.png b/addon/mc_mod/src/main/resources/assets/lumen/icon.png deleted file mode 100644 index 76f848a66eed620dcaa29e1b5497659992fbf189..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1689 zcmah}YgAKL7QR3T5C|fAc{->|krKj%AgS_L5XvJ!NOjDQz(w)TBgY6k-^$ z1cdNl`#jF(^*>Gk<&5I%}W(?Q_=N z``h0-jRg!U9v6ZG005tpolyh;F2t?v>E=WjUA0uF;TGhn6ORCFZY}|Ts|veFagTKcOLy&|@i*4_jUz$q=wWzi7`AiJ+Z@yZvD6T217cm`Bz;#A zgcZkXr0+j{+vtt+=nl4>9!Th<$3+NvHS6=APU9kZoZO7TaBEYO>h_(iz9wCxV??_) z0?%CjvSIE{C2K=kdv);F6Ze~R$w&0m7}S`3gje@^53}BK1?Z{qGV8}bf<9}<~xwy8` zxJjX-(fzvj|E8<*-KEU3%=?z!(v_k$$gTAMl;6_1I8QHA%WjB?rn1FCcU$ezTAy>p zHzHALuqHanE#=6;9k8tbHu25R)4NO#cUFQXTHIgUv=;wdB`Hl1z!LSiI=Gwgxh1b~ z=}z^}6L980TQEV;a{65gD^P!f=kin`O zjgYIdkgK5ycdzFXF3FIh)eobWwmdYsm_Ha~I|F=lqpJb755)D(gih0?f0}ZXG?8$>G_Nb3% zNM}9n@`dsF?Yxt!_muUS{K2J|Ry;BW6jW#&NXvrb;TLxZ%$1T>^$e1l9nUT&gwaM) zr@-3f?7mKw$n$sKSl${YekxcgNtSn8F<{HMpaQMrUDJ$}OqMfSfMNw%`YXOAnsjDp z@Yn?lqF2r4_P3h&iv1_vQTlLJMuC9Qawa)}0C<~M z<~BfmfZg%qvMNQ*tRRZiMkK`$K?(0KfLbLJvoyh*(jX2xG-P1R>^vDMIi+EYm%V!O z@@c#I0dwkLwql?f65+m?MIyBb7WBLCLw>=qAQt#wGwbbgsWDhVDTrhO-X^iCnm7e^ zEtpkjiS13@=7F!o3yH7KZH{%`L-#(as$Q?Rb|r$B$aIL^rCo&!M?T=rdzy~Nv!w)A zI7FSoZiWKsma-KD06&*?c(n|C$Xx6NwEh8)3^<7U@%b^GfPQUZRaGS3DoEli=;5MS zwLUG<+c%+S#aAQ~Ayj$PCw;FuLJk^a<6c41_~k003!#P8p&rr#1V5U<4h2#!T*M-M zEv3zmIo!jv(n6$GXj%L77v^-gHf7rbk0K7G%m1*zW6sr=6vXQn!V}<1Akdp%bgN>m5|aqDR>1R4tQiK zI`333!;zB8BlvmG4;zp(Xi}Oxmt-sE=MXq9XNc{3CkfS1J?@@a3mtl5ZgaY8m1v>M zDQBq18S4AH@TX}kum|HBlzw@x4))m;U=7k~rDUiEeB0NV%PvsR!2kc>qknao=o*f( R^%`|sSWYG*qwhVg_r`y~JX diff --git a/addon/mc_mod/src/main/resources/fabric.mod.json b/addon/mc_mod/src/main/resources/fabric.mod.json deleted file mode 100644 index 48eacfe..0000000 --- a/addon/mc_mod/src/main/resources/fabric.mod.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "schemaVersion": 1, - "id": "lumen", - "version": "${version}", - "name": "Lumen", - "description": "Support client for Project Lumen", - "authors": [ - "robcholz" - ], - "contact": { - "homepage": "https://github.com/robcholz/Lumen", - "issues": "https://github.com/robcholz/Lumen/issues" - }, - "license": "GPL-3.0", - "icon": "assets/lumen/icon.png", - "environment": "client", - "entrypoints": { - "client": [ - "com.robcholz.lumen.client.LumenClient" - ], - "modmenu": [ - "com.robcholz.lumen.client.LumenModMenuIntegration" - ] - }, - "mixins": [], - "depends": { - "fabricloader": ">=${loader_version}", - "fabric": "*", - "minecraft": "${minecraft_version}" - } -}