diff --git a/.gitignore b/.gitignore index 7e7d56a71..3f0dbfd57 100644 --- a/.gitignore +++ b/.gitignore @@ -133,3 +133,20 @@ run/ # Ignore datagen cache. /common/src/main/generated/resources/.cache/ + +/fabric/run +/fabric/runs +/forge/run +/forge/runs +/neoforge/run +/neoforge/runs +/common/run +/common/runs +/quilt/run +/quilt/runs +/bootimage/logs +/logs +*.log.gz +*.log +*.ext2 +/bootimage/src/fs/.idea/ diff --git a/.run/Run Boot Imager.run.xml b/.run/Run Boot Imager.run.xml new file mode 100644 index 000000000..a4de47f6f --- /dev/null +++ b/.run/Run Boot Imager.run.xml @@ -0,0 +1,15 @@ + + + + \ No newline at end of file diff --git a/bootimage/build.gradle b/bootimage/build.gradle new file mode 100644 index 000000000..085010112 --- /dev/null +++ b/bootimage/build.gradle @@ -0,0 +1,42 @@ +plugins { + id 'java' +} + +group = 'dev.ultreon.mods' +version = '0.9.0' + +repositories { + mavenCentral() +} + +sourceSets { + fs { + resources { + srcDirs = ['src/fs'] + } + java { + srcDirs.clear() + srcDirs = ['src/main/java'] + } + } +} + +processFsResources { + exclude ".idea" + exclude ".gradle" + exclude "build" + exclude "out" + exclude "**/__pycache__" + exclude "**/*.pyc" +} + +dependencies { + testImplementation platform('org.junit:junit-bom:5.10.0') + testImplementation 'org.junit.jupiter:junit-jupiter' + + implementation 'com.github.Nuix:jnode-fs:v1.0.1' +} + +test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/bootimage/src/fs/bin/echo.py b/bootimage/src/fs/bin/echo.py new file mode 100644 index 000000000..7a802f103 --- /dev/null +++ b/bootimage/src/fs/bin/echo.py @@ -0,0 +1,7 @@ +from libstd import main + + +@main +def prog_main(args: list[str]) -> int: + print(" ".join(args)) + return 0 diff --git a/bootimage/src/fs/bin/shell.py b/bootimage/src/fs/bin/shell.py new file mode 100644 index 000000000..e69de29bb diff --git a/bootimage/src/fs/boot/main.py b/bootimage/src/fs/boot/main.py new file mode 100644 index 000000000..2048be89b --- /dev/null +++ b/bootimage/src/fs/boot/main.py @@ -0,0 +1,80 @@ +import os + +def create_dir(path, mode): + if not os.path.exists(path): + os.makedirs(path, exist_ok=True) + + +# noinspection PyProtectedMember,PyUnresolvedReferences +def boot(): + import sys + create_dir("/bin", 0o755) + create_dir("/sys", 0o755) + create_dir("/sys/dev", 0o755) + create_dir("/sys/proc", 0o755) + create_dir("/sys/sys", 0o755) + + create_dir("/dev/pts", 0o755) + create_dir("/dev/serial", 0o755) + create_dir("/dev/usb", 0o755) + create_dir("/dev/virtual", 0o755) + + create_dir("/usr/bin", 0o755) + create_dir("/usr/lib", 0o755) + create_dir("/usr/sbin", 0o755) + create_dir("/usr/share", 0o755) + create_dir("/usr/local", 0o755) + create_dir("/usr/local/bin", 0o755) + create_dir("/usr/local/lib", 0o755) + + create_dir("/var", 0o755) + create_dir("/var/log", 0o755) + create_dir("/var/lib", 0o755) + create_dir("/var/lock", 0o755) + create_dir("/var/run", 0o755) + create_dir("/var/tmp", 0o755) + + create_dir("/etc", 0o755) + create_dir("/media", 0o755) + create_dir("/tmp", 0o755) + create_dir("/opt", 0o755) + create_dir("/home", 0o755) + create_dir("/home/setup", 0o755) + create_dir("/root", 0o755) + create_dir("/run", 0o755) + create_dir("/sbin", 0o755) + + sys.path.append("/usr/bin") + sys.path.append("/usr/lib") + sys.path.append("/bin") + sys.path.append("/lib") + sys.path.append("/usr/local/bin") + sys.path.append("/usr/local/lib") + sys.path.append("/sbin") + sys.path.append("/usr/sbin") + sys.path.append("/usr/local/sbin") + sys.path.append("/var/lib") + + if "/lib" not in sys.path: + raise Exception("Failed to add /lib to sys.path") + + import os + if not os.path.exists("/lib/libsystem.py"): + raise Exception("Failed to find /lib/libsystem.py") + + import importlib + + importlib.invalidate_caches() + libsystem = importlib.import_module("libsystem") + + # noinspection PyUnresolvedReferences + try: + libsystem._bootinit(bios) + except Exception as e: + print(e) + + while True: + pass + + +boot() diff --git a/bootimage/src/fs/home/user/.shellrc b/bootimage/src/fs/home/user/.shellrc new file mode 100644 index 000000000..e69de29bb diff --git a/bootimage/src/fs/lib/libstd.py b/bootimage/src/fs/lib/libstd.py new file mode 100644 index 000000000..5c7e0e1c4 --- /dev/null +++ b/bootimage/src/fs/lib/libstd.py @@ -0,0 +1 @@ +def main(self): diff --git a/bootimage/src/fs/lib/libsystem.py b/bootimage/src/fs/lib/libsystem.py new file mode 100644 index 000000000..684b9552e --- /dev/null +++ b/bootimage/src/fs/lib/libsystem.py @@ -0,0 +1,343 @@ +import threading +from typing import Callable + + +def readonly(self): + __setattr__ = self.__setattr__ + + def is_called_from(self): + import inspect + + if inspect.ismodule(self): + stack = inspect.stack() + return len(stack) > 1 and inspect.stack()[1][0].f_globals.get('__qualname__', None) == self.__qualname__ + elif inspect.ismethod(self): + stack = inspect.stack() + return len(stack) > 1 and inspect.stack()[1][0].f_locals.get('self', None) is self + elif inspect.isfunction(self): + stack = inspect.stack() + return len(stack) > 1 and inspect.stack()[1][0].f_locals.get('__qualname__', None) == self.__qualname__ + elif inspect.isclass(self): + stack = inspect.stack() + return len(stack) > 1 and inspect.stack()[1][0].f_locals.get('__qualname__', None) == self.__qualname__ + else: + return False + + def __internal_setattr__(self, key, value): + if key.startswith('_') and is_called_from(self): + raise AttributeError(f"Cannot set attribute {key}, module '{type(self).__name__}' is read-only") + + __setattr__(self, key, value) + + self.__setattr__ = __internal_setattr__ + + return self + + +# noinspection PyPep8Naming +@readonly +class InventoryApi: + def __init__(self): + # Native API + raise NotImplemented() + + def get(self, index: int) -> tuple[str, int]: + # Native API + raise NotImplemented() + + def count(self) -> int: + # Native API + raise NotImplemented() + + def find(self, name: str) -> int: + # Native API + raise NotImplemented() + + def findAll(self, name: str) -> list[int]: + # Native API + raise NotImplemented() + + def getName(self, index: int) -> str: + # Native API + raise NotImplemented() + + +# noinspection PyPep8Naming +@readonly +class ButtonAPI: + def __init__(self): + # Native API + raise NotImplemented() + + def setText(self, text: str): + # Native API + raise NotImplemented() + + def setEnabled(self, enabled: bool): + # Native API + raise NotImplemented() + + def setPressed(self, pressed: bool): + # Native API + raise NotImplemented() + + def isPressed(self) -> bool: + # Native API + raise NotImplemented() + + def isEnabled(self) -> bool: + # Native API + raise NotImplemented() + + def getText(self) -> str: + # Native API + raise NotImplemented() + + +# noinspection PyPep8Naming +@readonly +class LabelAPI: + def setText(self, text: str): + # Native API + raise NotImplemented() + + def getText(self) -> str: + # Native API + raise NotImplemented() + + def setEnabled(self, enabled: bool): + # Native API + raise NotImplemented() + + def isEnabled(self) -> bool: + # Native API + raise NotImplemented() + + +# noinspection PyPep8Naming +@readonly +class Image: + def __init__(self): + # Native API + raise NotImplemented() + + def getWidth(self) -> int: + # Native API + raise NotImplemented() + + def getHeight(self) -> int: + # Native API + raise NotImplemented() + + def getData(self) -> bytes: + # Native API + raise NotImplemented() + + def destroy(self): + # Native API + raise NotImplemented() + + +# noinspection PyPep8Naming +@readonly +class UiApi: + def __init__(self): + # Native API + raise NotImplemented() + + def createButton(self, x: int, y: int, width: int, height: int, text: str, normal: Image, pressed: Image, disabled: Image) -> ButtonAPI: + # Native API + raise NotImplemented() + + def createLabel(self, x: int, y: int, width: int, height: int, text: str) -> LabelAPI: + # Native API + raise NotImplemented() + + def createImage(self, x: int, y: int, width: int, height: int, data: bytes) -> Image: + # Native API + raise NotImplemented() + + def getImageLimit(self) -> int: + # Native API + raise NotImplemented() + + def getImageCount(self) -> int: + # Native API + raise NotImplemented() + + +# noinspection PyPep8Naming +@readonly +class ProcessAPI: + def __init__(self): + # Native API + raise NotImplemented() + + def setOnExit(self, callback: Callable[[int], None]): + # Native API + raise NotImplemented() + + def run(self): + # Native API + raise NotImplemented() + + +# noinspection PyPep8Naming +@readonly +class __BiosAPI: + def __init__(self): + # Native API + raise NotImplemented() + + def getInventoryApi(self) -> InventoryApi: + # Native API + raise NotImplemented() + + def getUiApi(self) -> UiApi: + # Native API + raise NotImplemented() + + def spawnProcess(self, modules, command: list[str], env: dict[str, str]) -> ProcessAPI: + # Native API + raise NotImplemented() + + +@readonly +def get_inventory() -> InventoryApi: + # Native API + raise NotImplemented() + + +@readonly +def get_ui() -> UiApi: + # Native API + raise NotImplemented() + + +@readonly +def request_shutdown(): + globals()['__shutdown_requested'] = True + + +class Process: + def __init__(self): + # Native API + raise NotImplemented() + + def wait(self): + # Native API + raise NotImplemented() + + def kill(self): + # Native API + raise NotImplemented() + + def update(self): + # Native API + raise NotImplemented() + + def __setattr__(self, key, value): + if not hasattr(self, key): + raise AttributeError(key) + + +def spawn_process(path: str): + import os + if not os.path.exists(path): + raise Exception(f"File {path} does not exist") + + return Process(path) + + +class PowerState: + __STATE = None + + @classmethod + def shutdown(cls): + cls.__STATE = 0 + + @classmethod + def reboot(cls): + cls.__STATE = 1 + + +def allows(privilege: str): + return True + + +def run_privileged(privilege: str, func): + pass + + +# noinspection PyUnresolvedReferences +def _bootinit(bios: __BiosAPI): + """ + This method is inaccessible after boot setup + :param bios: the bios api + :return: None + """ + import sys, os, os.path + + print("Booting...") + + globals()['get_inventory'] = bios.getInventoryApi + globals()['get_ui'] = bios.getUiApi + globals()['bootinit'] = None + globals()['__shutdown_requested'] = False + + print("Locking modules") + + current_process=None + + privileges = threading.local() + privileges._privilege = None + + class Privilege: + def __init__(self): + self._privilege = "" + + def process_end(proc: Process | None, code: int): + if proc is not None: + proc.wait() + sys.exit(code) + + sys.exit = lambda code: process_end(current_process, code) + + def do_not_allow_setattr(self, key, value): + raise AttributeError(f"Cannot set attribute {key}, module '{type(self).__name__}' is read-only") + + sys.modules['libsystem'].__setattr__ = do_not_allow_setattr + sys.modules['sys'].__setattr__ = do_not_allow_setattr + sys.modules['os'].__setattr__ = do_not_allow_setattr + sys.modules['os.path'].__setattr__ = do_not_allow_setattr + + item = sys.modules.__setitem__ + + def do_not_allow_setitem(key, value): + if key == 'libsystem' or key == 'sys' or key == 'os' or key == 'os.path': + raise AttributeError(f"Cannot set attribute module is read-only") + item(key, value) + + def do_not_allow_setattr_modules(self, key, value): + if key == "modules" or key == "__setitem__" or key == "__setattr__": + raise AttributeError(f"Cannot set attribute {key}, module '{type(self).__name__}' is read-only") + + sys.modules.__setitem__ = do_not_allow_setitem + sys.modules.__setattr__ = do_not_allow_setattr + sys.__setattr__ = do_not_allow_setattr_modules + + print("Launching processes...") + + processes: list[Process] = [] + + while True: + if globals()['__shutdown_requested']: + print("Shutting down...") + for p in processes: + p.kill() + break + + for p in processes: + p.update() + + print("Shutting down...") diff --git a/bootimage/src/fs/lib/libui/widgets.py b/bootimage/src/fs/lib/libui/widgets.py new file mode 100644 index 000000000..e69de29bb diff --git a/bootimage/src/fs/root/.shellrc b/bootimage/src/fs/root/.shellrc new file mode 100644 index 000000000..e69de29bb diff --git a/bootimage/src/main/java/dev/ultreon/bootimager/BootImager.java b/bootimage/src/main/java/dev/ultreon/bootimager/BootImager.java new file mode 100644 index 000000000..990d2a281 --- /dev/null +++ b/bootimage/src/main/java/dev/ultreon/bootimager/BootImager.java @@ -0,0 +1,42 @@ +package dev.ultreon.bootimager; + +import org.jnode.fs.FileSystemException; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +public class BootImager { + public static void main(String[] args) throws FileSystemException, IOException { + if (!Files.exists(Path.of("build"))) { + Files.createDirectory(Path.of("build")); + } + + if (Files.exists(Path.of("common/src/main/resources/data/devices/filesystems/main.ext2"))) { + Files.delete(Path.of("common/src/main/resources/data/devices/filesystems/main.ext2")); + } + + try (Ext2FS fs = Ext2FS.format(Path.of("common/src/main/resources/data/devices/filesystems/main.ext2"), 16L * 1024L * 1024L)) { + try (var walk = Files.walk(Path.of("bootimage/src/fs"))) { + walk.forEach(path -> { + try { + String replace = path.toString().replace("bootimage/src/fs/", ""); + if (replace.startsWith(".")) { + return; + } + Path rel = Path.of("/" + replace); + if (Files.isDirectory(path)) { + fs.createDirectory(rel); + return; + } + byte[] data = Files.readAllBytes(path); + fs.createFile(rel, data); + } catch (Exception e) { + e.printStackTrace(); + } + }); + } + fs.flush(); + } + } +} \ No newline at end of file diff --git a/bootimage/src/main/java/dev/ultreon/bootimager/Ext2FS.java b/bootimage/src/main/java/dev/ultreon/bootimager/Ext2FS.java new file mode 100644 index 000000000..4c2a555c8 --- /dev/null +++ b/bootimage/src/main/java/dev/ultreon/bootimager/Ext2FS.java @@ -0,0 +1,741 @@ +package dev.ultreon.bootimager; + +import com.google.common.base.Preconditions; +import net.minecraft.util.Unit; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jnode.driver.block.BlockDeviceAPI; +import org.jnode.driver.virtual.VirtualDevice; +import org.jnode.fs.*; +import org.jnode.fs.FileSystem; +import org.jnode.fs.FileSystemException; +import org.jnode.fs.ext2.*; +import org.jnode.fs.spi.AbstractFileSystem; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.ref.Cleaner; +import java.nio.ByteBuffer; +import java.nio.file.*; +import java.util.Collections; +import java.util.Iterator; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +public class Ext2FS implements FS { + private final FileSystem fs; + private final ConcurrentMap locks = new ConcurrentHashMap<>(); + + private Ext2FS(Ext2FileSystem fs) { + this.fs = fs; + + Cleaner cleaner = Cleaner.create(); + cleaner.register(this, () -> { + try { + fs.close(); + } catch (IOException e) { + LoggerFactory.getLogger(Ext2FS.class).error("Failed to close filesystem", e); + } + }); + } + + public static Ext2FS open(Path filePath) throws IOException, FileSystemException { + return open(false, filePath); + } + + public static Ext2FS open(boolean readOnly, Path filePath) throws IOException, FileSystemException { + var device = new VirtualDevice("MineDisk"); + var blockDevice = new VirtualBlockDevice(filePath.toFile().getAbsolutePath(), Files.size(filePath)); + device.registerAPI(BlockDeviceAPI.class, blockDevice); + + var type = new Ext2FileSystemType(); + var fs = new Ext2FileSystem(device, readOnly, type); + fs.read(); + return new Ext2FS(fs); + } + + public static Ext2FS openForced(Path filePath) throws IOException, FileSystemException { + var device = new VirtualDevice("MineDisk"); + var blockDevice = new VirtualBlockDevice(filePath.toFile().getAbsolutePath(), Files.size(filePath)); + device.registerAPI(BlockDeviceAPI.class, blockDevice); + + var type = new Ext2FileSystemType(); + var fs = new Ext2FileSystem(device, false, type); + fs.getSuperblock().setState(Ext2Constants.EXT2_VALID_FS); + fs.flush(); + fs.read(); + return new Ext2FS(fs); + } + + public static Ext2FS format(Path filePath, long diskSize) throws IOException, FileSystemException { + if (diskSize <= 16384) throw new IllegalArgumentException("Disk size must be greater than 16 KiB"); + + if (!Files.exists(filePath)) { + Files.createFile(filePath); + } + + var device = new VirtualDevice("MineDisk"); + device.registerAPI(BlockDeviceAPI.class, new VirtualBlockDevice(filePath.toFile().getAbsolutePath(), diskSize)); + + var blockDevice = new VirtualBlockDevice(filePath.toFile().getAbsolutePath(), diskSize); + var formatter = new Ext2FileSystemFormatter(BlockSize._1Kb); + var fs = formatter.format(device); + return new Ext2FS(fs); + } + + @Override + public void close() throws IOException { + fs.close(); + } + + @Override + public InputStream read(Path path, OpenOption... options) throws IOException { + FSFile file = getFileAt(path); + if (file == null) throw new NoSuchFileException("File not found: " + path); + return new FSInputStream(file, options); + } + + @Override + public OutputStream write(Path path, OpenOption... options) throws IOException { + boolean create = false; + boolean createNew = false; + for (OpenOption option : options) { + if (option == StandardOpenOption.CREATE) { + create = true; + } else if (option == StandardOpenOption.CREATE_NEW) { + createNew = true; + } + } + + if (create && createNew) throw new IllegalArgumentException("Cannot create and createNew"); + + FSFile file; + if (create) { + Ext2Directory parentDir = getDirectoryAt(path.getParent()); + if (parentDir == null) { + throw new NoSuchFileException(path.getParent().toString(), null, "parent directory not found"); + } + Ext2Entry entry = (Ext2Entry) parentDir.getEntry(path.getFileName().toString()); + if (entry == null) { + file = parentDir.addFile(path.getFileName().toString()).getFile(); + } else if (entry.isFile()) { + file = entry.getFile(); + } else { + throw new FileAlreadyExistsException("Directory already exists: " + path); + } + } else if (createNew) { + Ext2Directory parentDir = getDirectoryAt(path.getParent()); + if (parentDir == null) { + throw new NoSuchFileException(path.getParent().toString(), null, "parent directory not found"); + } + if (parentDir.getEntry(path.getFileName().toString()) != null) { + throw new FileAlreadyExistsException("File already exists: " + path); + } + file = parentDir.addFile(path.getFileName().toString()).getFile(); + } else { + file = getFileAt(path); + if (file == null) throw new NoSuchFileException(path.toString(), null, "file not found"); + } + return new FSOutputStream(file, options); + } + + @Override + public boolean exists(Path path) { + try { + return getFsEntry(path) != null; + } catch (IOException e) { + return false; + } + } + + private FSFile getFileAt(Path path) throws IOException { + Ext2Entry entry = getFsEntry(path); + if (entry == null) return null; + return entry.getFile(); + } + + private Ext2Directory getDirectoryAt(Path path) throws IOException { + Preconditions.checkNotNull(path, "path"); + Ext2Entry fsEntry = getFsEntry(path); + if (fsEntry == null) return null; + if (!fsEntry.isDirectory()) throw new IllegalArgumentException("Path is not a directory: " + path); + return (Ext2Directory) fsEntry.getDirectory(); + } + + private @Nullable Ext2Entry getFsEntry(Path path) throws IOException { + if (!path.toString().startsWith("/")) path = Path.of("/" + path); + if (!path.toString().startsWith("/")) path = Path.of("/" + path); + if (path.getParent() == null) return (Ext2Entry) fs.getRootEntry(); + + Ext2Directory root = (Ext2Directory) fs.getRootEntry().getDirectory(); + for (Path s : path.getParent()) { + Ext2Entry entry = (Ext2Entry) root.getEntry(s.toString()); + if (entry == null) return null; + if (!entry.isDirectory()) return null; + root = (Ext2Directory) entry.getDirectory(); + if (root == null) return null; + } + return (Ext2Entry) root.getEntry(path.getFileName().toString()); + } + + @Override + public void flush() throws IOException { + if (fs instanceof AbstractFileSystem) ((AbstractFileSystem) fs).flush(); + } + + @Override + public void createFile(Path path, byte[] data) throws IOException { + if (!path.toString().startsWith("/")) path = Path.of("/" + path); + if (path.toString().equals("/")) throw new IOException("Invalid path for file: " + path); + + Ext2Directory parentDir = path.getParent() == null ? (Ext2Directory) fs.getRootEntry().getDirectory() : getDirectoryAt(path.getParent()); + if (parentDir == null) + throw new NoSuchFileException(path.getParent().toString(), null, "parent directory not found"); + FSFile file = parentDir.addFile(path.getFileName().toString()).getFile(); + file.flush(); + ByteBuffer buffer = ByteBuffer.allocate(1024); + for (int i = 0; i < data.length; i += 1024) { + buffer.clear(); + buffer.put(data, i, Math.min(1024, data.length - i)); + buffer.flip(); + file.write(i, buffer); + file.flush(); + } + if (fs instanceof AbstractFileSystem absFs) absFs.flush(); + } + + @Override + public void createDirectory(Path path) throws IOException { + if (!path.toString().startsWith("/")) path = Path.of("/" + path); + Ext2Directory parentDir = path.getParent() == null ? (Ext2Directory) fs.getRootEntry().getDirectory() : getDirectoryAt(path.getParent()); + if (path.getParent() != null && parentDir != null) { + parentDir.addDirectory(path.getFileName().toString()); + parentDir.flush(); + } else { + FSDirectory directory = fs.getRootEntry().getDirectory(); + directory.addDirectory(path.getFileName().toString()); + directory.flush(); + } + if (fs instanceof AbstractFileSystem absFs) absFs.flush(); + } + + @Override + public Iterator listDirectory(Path of) throws IOException { + Ext2Directory dir = getDirectoryAt(of); + if (dir == null) return Collections.emptyIterator(); + return new DirNameIterator(dir); + } + + @Override + public void delete(Path path) throws IOException { + Ext2Entry parent = path.getParent() == null ? (Ext2Entry) fs.getRootEntry() : getFsEntry(path.getParent()); + if (parent == null) return; + + if (!parent.isDirectory()) throw new NotDirectoryException("Path is not a directory: " + path); + + Ext2Entry entry = (Ext2Entry) parent.getDirectory().getEntry(path.getFileName().toString()); + if (entry.isDirectory()) { + if (entry.getDirectory().iterator().hasNext()) { + throw new DirectoryNotEmptyException("Directory is not empty: " + path); + } + + parent.getDirectory().remove(path.getFileName().toString()); + return; + } + + if (!entry.isFile()) throw new IOException("Path is not a file: " + path); + parent.getDirectory().remove(path.getFileName().toString()); + } + + @Override + public long size(Path path) throws IOException { + Ext2Entry entry = getFsEntry(path); + if (entry == null) return -1; + if (!entry.isFile()) return -1; + return entry.getFile().getLength(); + } + + @Override + public void rename(Path from, String name) throws IOException { + if (name.contains("/")) throw new IOException("Invalid name: " + name); + + Ext2Entry parent = from.getParent() == null ? (Ext2Entry) fs.getRootEntry() : getFsEntry(from.getParent()); + if (parent == null) return; + + if (!parent.isDirectory()) throw new NotDirectoryException("Path is not a directory: " + from); + + Ext2Entry entry = (Ext2Entry) parent.getDirectory().getEntry(from.getFileName().toString()); + if (entry == null) return; + entry.setName(name); + parent.getDirectory().flush(); + } + + @Override + public boolean isFolder(Path path) throws IOException { + Ext2Entry entry = getFsEntry(path); + return entry != null && entry.isDirectory(); + } + + @Override + public boolean isFile(Path path) throws IOException { + Ext2Entry entry = getFsEntry(path); + return entry != null && entry.isFile(); + } + + @Override + public boolean isSymbolicLink(Path path) { + return false; + } + + @Override + public @Nullable LockKey lock(String path) throws IOException { + if (path == null) throw new IllegalArgumentException("Path must not be null"); + Path pathObj = Path.of(path); + if (!exists(pathObj)) return null; + if (this.locks.containsKey(pathObj)) + throw new IOException("Path is already locked: " + pathObj); + this.locks.put(pathObj, Unit.INSTANCE); + return new LockKey(pathObj); + } + + @Override + public void unlock(String directory) { + if (directory == null) throw new IllegalArgumentException("Path must not be null"); + this.locks.remove(Path.of(directory)); + } + + @Override + public boolean isLocked(String directory) { + return this.locks.containsKey(Path.of(directory)); + } + + @Override + public void setReadOnly(Path of, boolean b) throws IOException { + @Nullable Ext2Entry dir = getFsEntry(of); + if (dir == null) return; + dir.getAccessRights().setReadable(true, false); + dir.getAccessRights().setWritable(!b, false); + dir.getParent().flush(); + } + + @Override + public void setExecutable(Path of, boolean b) throws IOException { + @Nullable Ext2Entry dir = getFsEntry(of); + if (dir == null) return; + if (dir.isFile()) { + dir.getAccessRights().setExecutable(b, false); + dir.getParent().flush(); + } else { + throw new IOException("Path is not a file: " + of); + } + } + + @Override + public boolean isExecutable(Path of) throws IOException { + Ext2Entry entry = getFsEntry(of); + if (entry == null) return false; + return entry.isFile() && entry.getAccessRights().canExecute(); + } + + @Override + public boolean isWritable(Path of) throws IOException { + Ext2Entry entry = getFsEntry(of); + if (entry == null) return false; + return entry.isFile() && entry.getAccessRights().canWrite(); + } + + @Override + public boolean isReadable(Path of) throws IOException { + Ext2Entry entry = getFsEntry(of); + if (entry == null) return false; + return entry.isFile() && entry.getAccessRights().canRead(); + } + + @Override + public int getOwner(Path of) throws IOException { + Ext2Entry entry = getFsEntry(of); + if (entry == null) throw new IOException("Path does not exist: " + of); + return entry.getINode().getUid(); + } + + @Override + public int getGroup(Path of) throws IOException { + Ext2Entry entry = getFsEntry(of); + if (entry == null) throw new IOException("Path does not exist: " + of); + return entry.getINode().getGid(); + } + + @Override + public int getPermissions(Path of) throws IOException { + Ext2Entry entry = getFsEntry(of); + if (entry == null) throw new IOException("Path does not exist: " + of); + return entry.getINode().getMode(); + } + + @Override + public void setPermissions(Path of, int mode) throws IOException { + Ext2Entry entry = getFsEntry(of); + if (entry == null) throw new IOException("Path does not exist: " + of); + entry.getINode().setMode(mode); + entry.getParent().flush(); + } + + @Override + public void setOwner(Path of, int uid, int gid) throws IOException { + Ext2Entry entry = getFsEntry(of); + if (entry == null) throw new IOException("Path does not exist: " + of); + entry.getINode().setUid(uid); + entry.getINode().setGid(gid); + entry.getParent().flush(); + } + + @Override + public void setGroup(Path of, int gid) throws IOException { + Ext2Entry entry = getFsEntry(of); + if (entry == null) throw new IOException("Path does not exist: " + of); + entry.getINode().setGid(gid); + entry.getParent().flush(); + } + + @Override + public void setOwner(Path of, int uid) throws IOException { + Ext2Entry entry = getFsEntry(of); + if (entry == null) throw new IOException("Path does not exist: " + of); + entry.getINode().setUid(uid); + entry.getParent().flush(); + } + + @Override + public long getGeneration(Path of) throws IOException { + Ext2Entry entry = getFsEntry(of); + if (entry == null) throw new IOException("Path does not exist: " + of); + return entry.getINode().getGeneration(); + } + + @Override + public void setGeneration(Path of, long generation) throws IOException { + Ext2Entry entry = getFsEntry(of); + if (entry == null) throw new IOException("Path does not exist: " + of); + entry.getINode().setGeneration(generation); + entry.getParent().flush(); + } + + @Override + public boolean isReadOnly(Path of) throws IOException { + Ext2Entry entry = getFsEntry(of); + if (entry == null) throw new IOException("Path does not exist: " + of); + return entry.getAccessRights().canRead() && !entry.getAccessRights().canWrite(); + } + + @Override + public boolean canWrite(Path of) throws IOException { + return isWritable(of); + } + + @Override + public boolean canRead(Path of) throws IOException { + return isReadable(of); + } + + @Override + public boolean canExecute(Path of) throws IOException { + Ext2Entry entry = getFsEntry(of); + if (entry == null) return false; + return entry.getAccessRights().canExecute(); + } + + @Override + public long lastModified(Path path) throws IOException { + Ext2Entry entry = getFsEntry(path); + if (entry == null) return 0; + return entry.getLastModified(); + } + + @Override + public long lastAccessed(Path path) throws IOException { + Ext2Entry entry = getFsEntry(path); + if (entry == null) return 0; + return entry.getLastAccessed(); + } + + @Override + public long creationTime(Path path) throws IOException { + Ext2Entry entry = getFsEntry(path); + if (entry == null) return 0; + return entry.getINode().getCtime(); + } + + @Override + public void setLastAccessed(Path path, long time) throws IOException { + Ext2Entry entry = getFsEntry(path); + if (entry == null) return; + entry.setLastAccessed(time); + entry.getParent().flush(); + } + + @Override + public void setLastModified(Path path, long time) throws IOException { + Ext2Entry entry = getFsEntry(path); + if (entry == null) return; + entry.setLastModified(time); + entry.getParent().flush(); + } + + @Override + public void setCreationTime(Path path, long time) throws IOException { + Ext2Entry entry = getFsEntry(path); + if (entry == null) return; + entry.getINode().setCtime(time); + entry.getParent().flush(); + } + + @Override + public long getTotalSpace() throws IOException { + return fs.getTotalSpace(); + } + + @Override + public long getUsableSpace() throws IOException { + return fs.getUsableSpace(); + } + + @Override + public long getFreeSpace() throws IOException { + return fs.getFreeSpace(); + } + + @Override + public void move(Path source, Path destination) throws IOException { + Ext2Entry sourceEntry = getFsEntry(source); + Ext2Entry destinationEntry = getFsEntry(destination); + if (sourceEntry == null || destinationEntry == null) return; + + try (InputStream in = read(source)) { + createFile(destination, in.readAllBytes()); + } + + sourceEntry.getParent().remove(sourceEntry.getName()); + sourceEntry.getParent().flush(); + destinationEntry.getParent().flush(); + } + + @Override + public void copy(Path source, Path destination) throws IOException { + Ext2Entry sourceEntry = getFsEntry(source); + Ext2Entry destinationEntry = getFsEntry(destination); + if (sourceEntry == null || destinationEntry == null) return; + + try (InputStream in = read(source)) { + createFile(destination, in.readAllBytes()); + } + + sourceEntry.getParent().flush(); + destinationEntry.getParent().flush(); + } + + @Override + public void write(Path path, long offset, byte[] dataBytes) throws IOException { + Ext2Entry entry = getFsEntry(path); + if (entry == null) return; + + if (entry.isFile()) { + FSFile file = entry.getFile(); + long length = file.getLength(); + if (offset > length) throw new IOException("Offset out of range: " + offset); + if (offset + dataBytes.length > length) { + file.setLength(offset + dataBytes.length); + } + file.write(offset, ByteBuffer.wrap(dataBytes)); + file.flush(); + entry.getParent().flush(); + } else { + throw new IOException("Path is not a file: " + path); + } + } + + @Override + public void write(Path path, byte[] dataBytes) throws IOException { + write(path, 0, dataBytes); + } + + @Override + public void truncate(Path path, long size) throws IOException { + Ext2Entry entry = getFsEntry(path); + if (entry == null) return; + if (entry.isFile()) { + entry.getFile().setLength(size); + } + } + + @Override + public void read(Path path, ByteBuffer buffer, long offset) throws IOException { + Ext2Entry entry = getFsEntry(path); + if (entry == null) return; + if (entry.isFile()) { + entry.getFile().read(offset, buffer); + } + } + + public FileSystem getFileSystem() { + return fs; + } + + private static class FSInputStream extends InputStream { + private final FSFile file; + private final ByteBuffer buffer = ByteBuffer.allocate(1024); + private long bufferOffset; + private long fileOffset = 0; + + private int markLimit; + + public FSInputStream(FSFile file, OpenOption[] options) throws IOException { + this.file = file; + for (OpenOption option : options) { + if (option == StandardOpenOption.TRUNCATE_EXISTING) { + file.setLength(0); + } else if (option != StandardOpenOption.READ) { + throw new UnsupportedOperationException("Option not supported: " + option); + } + } + + bufferOffset = 0; + buffer.clear(); + if (fileOffset + buffer.capacity() > file.getLength()) { + buffer.limit((int) (file.getLength() - fileOffset)); + } + file.read(fileOffset, buffer); + buffer.flip(); + } + + @Override + public int read() throws IOException { + if (bufferOffset == buffer.limit()) { + bufferOffset = 0; + buffer.clear(); + if (fileOffset + buffer.capacity() > file.getLength()) { + buffer.limit((int) (file.getLength() - fileOffset)); + } + file.read(fileOffset, buffer); + fileOffset += buffer.capacity(); + } + + return buffer.get() & 0xFF; + } + + @Override + public int read(byte @NotNull [] b, int off, int len) throws IOException { + int bytesRead = 0; + if (fileOffset >= file.getLength()) { + return -1; + } + while (bytesRead < len) { + if (fileOffset >= file.getLength()) { + return bytesRead; + } + + int remaining = buffer.remaining(); + int len0 = Math.min(len - bytesRead, remaining); + if (len0 == 0) { + return -1; + } + int fileRemaining = (int) (file.getLength() - fileOffset); + int len1 = Math.min(len0, fileRemaining); + buffer.get(b, off + bytesRead, len1); + bufferOffset += len1; + bytesRead += len1; + fileOffset += len1; + if (bufferOffset == buffer.capacity()) { + bufferOffset = 0; + buffer.clear(); + if (fileOffset + buffer.capacity() > file.getLength()) { + buffer.limit((int) (file.getLength() - fileOffset)); + } + file.read(fileOffset, buffer); + } + + if (len0 < remaining) { + break; + } + } + return bytesRead; + } + + @Override + public int read(byte @NotNull [] b) throws IOException { + return read(b, 0, b.length); + } + } + + private static class DirNameIterator implements Iterator { + private final Iterator iterator; + + public DirNameIterator(Ext2Directory dir) throws IOException { + iterator = dir.iterator(); + } + + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public String next() { + return iterator.next().getName(); + } + } + + private class FSOutputStream extends OutputStream { + private final FSFile file; + private final ByteBuffer buffer = ByteBuffer.allocate(1024); + private boolean sync = false; + private long bufferOffset = 0; + private long fileOffset = 0; + + public FSOutputStream(FSFile file, OpenOption[] options) throws IOException { + this.file = file; + for (OpenOption option : options) { + if (option == StandardOpenOption.TRUNCATE_EXISTING) { + file.setLength(0); + } else if (option == StandardOpenOption.APPEND) { + fileOffset = file.getLength(); + } else if (option == StandardOpenOption.SYNC) { + sync = true; + } else if (option != StandardOpenOption.WRITE && option != StandardOpenOption.CREATE && option != StandardOpenOption.CREATE_NEW) { + throw new UnsupportedOperationException("Option not supported: " + option); + } + } + } + + @Override + public void write(int b) throws IOException { + buffer.put((byte) b); + bufferOffset++; + if (bufferOffset == buffer.capacity()) { + buffer.flip(); + file.write(fileOffset, buffer); + buffer.clear(); + bufferOffset = 0; + fileOffset += buffer.capacity(); + } + } + + @Override + public void write(byte @NotNull [] b) throws IOException { + write(b, 0, b.length); + } + + @Override + public void flush() throws IOException { + file.flush(); + if (fs instanceof AbstractFileSystem absFs) absFs.flush(); + } + + @Override + public void close() throws IOException { + file.flush(); + if (fs instanceof AbstractFileSystem absFs) absFs.flush(); + } + } +} diff --git a/bootimage/src/main/java/dev/ultreon/bootimager/FS.java b/bootimage/src/main/java/dev/ultreon/bootimager/FS.java new file mode 100644 index 000000000..a2b37126b --- /dev/null +++ b/bootimage/src/main/java/dev/ultreon/bootimager/FS.java @@ -0,0 +1,130 @@ +package dev.ultreon.bootimager; + +import org.jetbrains.annotations.Nullable; +import org.jnode.fs.FileSystemException; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.util.Iterator; + +public interface FS extends AutoCloseable { + void close() throws IOException; + + InputStream read(Path path, OpenOption... options) throws IOException; + + OutputStream write(Path path, OpenOption... options) throws IOException; + + boolean exists(Path path) throws IOException; + + void flush() throws IOException; + + void createFile(Path path, byte[] data) throws IOException; + + void createDirectory(Path path) throws IOException; + + Iterator listDirectory(Path of) throws IOException; + + void delete(Path path) throws IOException; + + long size(Path path) throws IOException; + + void rename(Path from, String name) throws IOException; + + boolean isFolder(Path path) throws IOException; + + boolean isFile(Path path) throws IOException; + + boolean isSymbolicLink(Path path) throws IOException; + + @Nullable LockKey lock(String path) throws IOException; + + void unlock(String directory); + + boolean isLocked(String directory); + + void setReadOnly(Path of, boolean b) throws IOException; + + void setExecutable(Path of, boolean b) throws IOException; + + boolean isExecutable(Path of) throws IOException; + + boolean isWritable(Path of) throws IOException; + + boolean isReadable(Path of) throws IOException; + + int getOwner(Path of) throws IOException; + + int getGroup(Path of) throws IOException; + + int getPermissions(Path of) throws IOException; + + void setPermissions(Path of, int mode) throws IOException; + + void setOwner(Path of, int uid, int gid) throws IOException; + + void setGroup(Path of, int gid) throws IOException; + + void setOwner(Path of, int uid) throws IOException; + + long getGeneration(Path of) throws IOException; + + void setGeneration(Path of, long generation) throws IOException; + + boolean isReadOnly(Path of) throws IOException; + + boolean canWrite(Path of) throws IOException; + + boolean canRead(Path of) throws IOException; + + boolean canExecute(Path of) throws IOException; + + static FS loadExt2(Path path) throws IOException, FileSystemException { + return Ext2FS.open(false, path); + } + + static FS loadExt2Forced(Path path) throws IOException, FileSystemException { + return Ext2FS.openForced(path); + } + + static FS loadExt2ReadOnly(Path path) throws IOException, FileSystemException { + return Ext2FS.open(true, path); + } + + static FS formatExt2(Path path, long diskSize) throws IOException, FileSystemException { + return Ext2FS.format(path, diskSize); + } + + long lastModified(Path path) throws IOException; + + long lastAccessed(Path path) throws IOException; + + long creationTime(Path path) throws IOException; + + void setLastAccessed(Path path, long time) throws IOException; + + void setLastModified(Path path, long time) throws IOException; + + void setCreationTime(Path path, long time) throws IOException; + + long getTotalSpace() throws IOException; + + long getUsableSpace() throws IOException; + + long getFreeSpace() throws IOException; + + void move(Path source, Path destination) throws IOException; + + void copy(Path source, Path destination) throws IOException; + + void write(Path path, long offset, byte[] dataBytes) throws IOException; + + void write(Path path, byte[] dataBytes) throws IOException; + + void truncate(Path path, long size) throws IOException; + + void read(Path path, ByteBuffer buffer, long offset) throws IOException; +} diff --git a/bootimage/src/main/java/dev/ultreon/bootimager/LockKey.java b/bootimage/src/main/java/dev/ultreon/bootimager/LockKey.java new file mode 100644 index 000000000..ea883e7b9 --- /dev/null +++ b/bootimage/src/main/java/dev/ultreon/bootimager/LockKey.java @@ -0,0 +1,16 @@ +package dev.ultreon.bootimager; + +import java.nio.file.Path; + +@SuppressWarnings("ClassCanBeRecord") +public class LockKey { + private final Path path; + + public LockKey(Path path) { + this.path = path; + } + + public Path getPath() { + return path; + } +} diff --git a/bootimage/src/main/java/dev/ultreon/bootimager/VirtualBlockDevice.java b/bootimage/src/main/java/dev/ultreon/bootimager/VirtualBlockDevice.java new file mode 100644 index 000000000..7e8c2bab6 --- /dev/null +++ b/bootimage/src/main/java/dev/ultreon/bootimager/VirtualBlockDevice.java @@ -0,0 +1,65 @@ +package dev.ultreon.bootimager; + +import org.jnode.driver.block.BlockDeviceAPI; + +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.Path; + +public class VirtualBlockDevice implements BlockDeviceAPI { + private final RandomAccessFile file; + + public VirtualBlockDevice(String filePath, long size) throws IOException { + Path path = Path.of(filePath); + if (Files.notExists(path)) { + Files.createFile(path); + } + this.file = new RandomAccessFile(filePath, "rw"); + + // Ensure the file is initialized to the specified size + if (file.length() < size) { + file.setLength(size); + } + } + + @Override + public long getLength() throws IOException { + return file.length(); + } + + @Override + public void read(long devOffset, ByteBuffer dest) throws IOException { + if (devOffset < 0 || devOffset >= file.length()) { + throw new IOException("Invalid read offset"); + } + file.seek(devOffset); + byte[] buffer = new byte[dest.remaining()]; + int bytesRead = file.read(buffer); + if (bytesRead < 0) { + throw new IOException("End of file reached"); + } + dest.put(buffer, 0, bytesRead); + } + + @Override + public void write(long devOffset, ByteBuffer src) throws IOException { + if (devOffset < 0 || devOffset >= file.length()) { + throw new IOException("Invalid write offset"); + } + file.seek(devOffset); + byte[] buffer = new byte[src.remaining()]; + src.get(buffer); + file.write(buffer); + } + + @Override + public void flush() throws IOException { + file.getFD().sync(); + } + + public void close() throws IOException { + file.close(); + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index ecd7ddc8f..6594b5b8d 100644 --- a/build.gradle +++ b/build.gradle @@ -22,6 +22,16 @@ Object getModDescription() { return mod_description } +tasks.register("runBootImager", JavaExec) { + classpath(files(rootProject.project("bootimage").sourceSets.main.output + project(":bootimage").sourceSets.main.runtimeClasspath)) + mainClass = "dev.ultreon.bootimager.BootImager" +} + +project("common").tasks.withType(ProcessResources).configureEach { + dependsOn ":runBootImager" +} + + subprojects { apply plugin: "dev.architectury.loom" diff --git a/common/build.gradle b/common/build.gradle index d7641c8f2..46cef1b17 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -18,6 +18,8 @@ dependencies { // Remove the next line if you don't want to depend on the API modApi "dev.architectury:architectury:$architectury_version" + implementation "org.graalvm.polyglot:polyglot:24.1.1" + implementation "org.graalvm.python:python-community:24.1.1" implementation "org.jetbrains.kotlin:kotlin-reflect:1.7.10" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10" // modApi "com.ultreon:ultranlang:0.1.0+6" diff --git a/common/src/main/java/com/ultreon/devices/BuiltinApps.java b/common/src/main/java/com/ultreon/devices/BuiltinApps.java index a2815f920..43a81afc9 100644 --- a/common/src/main/java/com/ultreon/devices/BuiltinApps.java +++ b/common/src/main/java/com/ultreon/devices/BuiltinApps.java @@ -4,11 +4,13 @@ import com.ultreon.devices.programs.BoatRacersApp; import com.ultreon.devices.programs.NoteStashApp; import com.ultreon.devices.programs.PixelPainterApp; +import com.ultreon.devices.programs.activation.ActivationApp; import com.ultreon.devices.programs.auction.MineBayApp; import com.ultreon.devices.programs.email.EmailApp; import com.ultreon.devices.programs.gitweb.GitWebApp; import com.ultreon.devices.programs.snake.SnakeApp; import com.ultreon.devices.programs.system.*; +import com.ultreon.devices.programs.terminal.TerminalApp; import com.ultreon.devices.programs.themes.ThemesApp; import dev.architectury.platform.Platform; import net.minecraft.resources.ResourceLocation; @@ -18,6 +20,7 @@ public static void registerBuiltinApps() { ApplicationManager.registerApplication(ResourceLocation.fromNamespaceAndPath(Reference.MOD_ID, "diagnostics"), () -> DiagnosticsApp::new, true); ApplicationManager.registerApplication(ResourceLocation.fromNamespaceAndPath(Reference.MOD_ID, "settings"), () -> SettingsApp::new, true); ApplicationManager.registerApplication(ResourceLocation.fromNamespaceAndPath(Reference.MOD_ID, "file_browser"), () -> FileBrowserApp::new, true); + ApplicationManager.registerApplication(ResourceLocation.fromNamespaceAndPath(Reference.MOD_ID, "terminal"), () -> TerminalApp::new, false); ApplicationManager.registerApplication(ResourceLocation.fromNamespaceAndPath(Reference.MOD_ID, "gitweb"), () -> GitWebApp::new, false); ApplicationManager.registerApplication(ResourceLocation.fromNamespaceAndPath(Reference.MOD_ID, "note_stash"), () -> NoteStashApp::new, false); ApplicationManager.registerApplication(ResourceLocation.fromNamespaceAndPath(Reference.MOD_ID, "pixel_painter"), () -> PixelPainterApp::new, false); diff --git a/common/src/main/java/com/ultreon/devices/Devices.java b/common/src/main/java/com/ultreon/devices/Devices.java index 096558ccf..62c334c26 100644 --- a/common/src/main/java/com/ultreon/devices/Devices.java +++ b/common/src/main/java/com/ultreon/devices/Devices.java @@ -5,6 +5,7 @@ import com.google.gson.*; import com.mojang.serialization.Lifecycle; import com.ultreon.devices.api.ApplicationManager; +import com.ultreon.devices.api.DeviceAPI; import com.ultreon.devices.api.app.Application; import com.ultreon.devices.api.print.IPrint; import com.ultreon.devices.api.print.PrintingManager; @@ -30,6 +31,7 @@ import com.ultreon.devices.programs.IconsApp; import com.ultreon.devices.programs.PixelPainterApp; import com.ultreon.devices.programs.TestApp; +import com.ultreon.devices.programs.activation.TaskActivateMineOS; import com.ultreon.devices.programs.auction.task.TaskAddAuction; import com.ultreon.devices.programs.auction.task.TaskBuyItem; import com.ultreon.devices.programs.auction.task.TaskGetAuctions; @@ -64,13 +66,24 @@ import net.minecraft.world.item.Items; import net.minecraft.world.item.crafting.RecipeType; import net.minecraft.world.level.Level; +import org.graalvm.polyglot.Context; +import org.graalvm.polyglot.Engine; +import org.graalvm.polyglot.Language; +import org.graalvm.polyglot.Value; +import org.graalvm.polyglot.io.FileSystem; +import org.graalvm.polyglot.io.IOAccess; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.IOException; import java.lang.reflect.Constructor; +import java.net.URI; +import java.nio.channels.SeekableByteChannel; +import java.nio.file.*; +import java.nio.file.attribute.FileAttribute; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicReference; @@ -219,6 +232,9 @@ private void registerApplications() { TaskManager.registerTask(TaskDeleteEmail::new); TaskManager.registerTask(TaskViewEmail::new); + // Activation Stuff + TaskManager.registerTask(TaskActivateMineOS::new); + if (Platform.isDevelopmentEnvironment() || Devices.EARLY_CONFIG.enableBetaApps) { // Auction TaskManager.registerTask(TaskAddAuction::new); diff --git a/common/src/main/java/com/ultreon/devices/api/DeviceAPI.java b/common/src/main/java/com/ultreon/devices/api/DeviceAPI.java new file mode 100644 index 000000000..e7f30d380 --- /dev/null +++ b/common/src/main/java/com/ultreon/devices/api/DeviceAPI.java @@ -0,0 +1,38 @@ +package com.ultreon.devices.api; + +import com.ultreon.devices.api.io.Drive; +import com.ultreon.devices.api.io.DriveRoot; +import com.ultreon.devices.api.io.FSResponse; +import com.ultreon.devices.core.DataPath; +import com.ultreon.devices.core.Laptop; +import com.ultreon.devices.object.Result; + +import java.nio.file.Path; +import java.util.function.Consumer; + +public class DeviceAPI { + private final Laptop instance; + + public DeviceAPI(Laptop instance) { + this.instance = instance; + } + + public void openFile(String path, Consumer> response) { + Drive mainDrive = Laptop.getMainDrive(); + if (mainDrive != null) { + mainDrive.read(Path.of(path), fsResponse -> { + boolean success = fsResponse.success(); + if (!success) { + response.accept(new Result<>(fsResponse.message(), null, false)); + return; + } + + byte[] data = fsResponse.data(); + FileHandle handle = new FileHandle(path, data); + response.accept(new Result<>(null, handle, true)); + }); + } else { + response.accept(new Result<>("No main drive", null, false)); + } + } +} diff --git a/common/src/main/java/com/ultreon/devices/api/FileHandle.java b/common/src/main/java/com/ultreon/devices/api/FileHandle.java new file mode 100644 index 000000000..892dc9f02 --- /dev/null +++ b/common/src/main/java/com/ultreon/devices/api/FileHandle.java @@ -0,0 +1,48 @@ +package com.ultreon.devices.api; + +public class FileHandle { + private final String path; + private final byte[] data; + + private int offset = 0; + private boolean closed; + + public FileHandle(String path, byte[] data) { + this.path = path; + this.data = data; + } + + public int tell() { + return offset; + } + + public void seek(int offset) { + this.offset = offset; + } + + public byte[] read(int length) { + if (closed) { + throw new IllegalStateException("File is closed"); + } + byte[] result = new byte[length]; + System.arraycopy(data, offset, result, 0, length); + offset += length; + return result; + } + + public void write(byte[] data) { + if (closed) { + throw new IllegalStateException("File is closed"); + } + System.arraycopy(data, 0, this.data, offset, data.length); + offset += data.length; + } + + public String getPath() { + return path; + } + + public void close() { + this.closed = true; + } +} diff --git a/common/src/main/java/com/ultreon/devices/api/FileSystemAPI.java b/common/src/main/java/com/ultreon/devices/api/FileSystemAPI.java new file mode 100644 index 000000000..8d37a7859 --- /dev/null +++ b/common/src/main/java/com/ultreon/devices/api/FileSystemAPI.java @@ -0,0 +1,6 @@ +package com.ultreon.devices.api; + +import com.ultreon.devices.core.Laptop; + +public class FileSystemAPI { +} diff --git a/common/src/main/java/com/ultreon/devices/api/app/Application.java b/common/src/main/java/com/ultreon/devices/api/app/Application.java index c0ade7346..125018b42 100644 --- a/common/src/main/java/com/ultreon/devices/api/app/Application.java +++ b/common/src/main/java/com/ultreon/devices/api/app/Application.java @@ -131,8 +131,11 @@ public void render(GuiGraphics graphics, Laptop laptop, Minecraft mc, int x, int // GL11.glEnable(GL11.GL_SCISSOR_TEST); GLHelper.pushScissor(x, y, width, height); - currentLayout.render(graphics, laptop, mc, x, y, mouseX, mouseY, active, partialTicks); - GLHelper.popScissor(); + try { + currentLayout.render(graphics, laptop, mc, x, y, mouseX, mouseY, active, partialTicks); + } finally { + GLHelper.popScissor(); + } // TODO Port this to 1.18.2 if possible // if (!GLHelper.isScissorStackEmpty()) { diff --git a/common/src/main/java/com/ultreon/devices/api/app/Dialog.java b/common/src/main/java/com/ultreon/devices/api/app/Dialog.java index b3ce02ee2..4e1529b0d 100644 --- a/common/src/main/java/com/ultreon/devices/api/app/Dialog.java +++ b/common/src/main/java/com/ultreon/devices/api/app/Dialog.java @@ -43,8 +43,8 @@ public abstract class Dialog extends Wrappable { protected final ColorScheme colorScheme = Laptop.getInstance().getSettings().getColorScheme(); protected final Layout defaultLayout; private String title = "Message"; - private int width; - private int height; + protected int width; + protected int height; private Layout customLayout; private boolean pendingLayoutUpdate = true; @@ -172,6 +172,7 @@ public void updateComponents(int x, int y) { public void close() { this.pendingClose = true; + this.getWindow().close(); } /// The response listener interface. Used for handling responses diff --git a/common/src/main/java/com/ultreon/devices/api/io/Drive.java b/common/src/main/java/com/ultreon/devices/api/io/Drive.java index d1d12c267..86cd7b7c2 100644 --- a/common/src/main/java/com/ultreon/devices/api/io/Drive.java +++ b/common/src/main/java/com/ultreon/devices/api/io/Drive.java @@ -19,6 +19,7 @@ import static com.ultreon.devices.core.io.FileSystem.request; public class Drive { + public static final int BUFFER_SIZE = 1024; private final String name; private final UUID uuid; private final Type type; @@ -169,7 +170,7 @@ public void rename(Path path, String name, Consumer> callback) } public void write(Path path, byte[] data, Consumer> callback) { - if (data.length < 1024) request(uuid, FileAction.Factory.makeWrite(path, -1, data), callback); + if (data.length < BUFFER_SIZE) request(uuid, FileAction.Factory.makeWrite(path, -1, data), callback); else if (data.length < 4 * 1024 * 1024) writeLarge(path, data, callback); else callback.accept(new FSResponse<>(false, FileSystem.Status.TOO_LARGE, null, "File too large")); } @@ -181,7 +182,7 @@ public void read(Path path, Consumer> callback) { return; } - if (info.data().getSize() >= 1024) { + if (info.data().getSize() >= BUFFER_SIZE) { readLarge(path, info.data().getSize(), callback); } else { request(uuid, FileAction.Factory.makeRead(path), callback); @@ -191,27 +192,10 @@ public void read(Path path, Consumer> callback) { private void readLarge(Path path, long size, Consumer> callback) { byte[] output = new byte[(int) size]; - AtomicInteger offset = new AtomicInteger(0); - int length = (int) Math.min(1024, size - offset.get()); - var ref = new Object() { - void fsResponseConsumer(FSResponse r) { - if (!r.success()) { - callback.accept(r); - return; - } - - int len = r.data().length; - System.arraycopy(r.data(), 0, output, offset.get(), len); - - if (offset.get() == length) { - callback.accept(new FSResponse<>(true, FileSystem.Status.SUCCESSFUL, output, "")); - return; - } - - request(uuid, FileAction.Factory.makeRead(path, offset.getAndSet(offset.get() + r.data().length) + r.data().length, (int) Math.min(1024, size - offset.get())), this::fsResponseConsumer); - } - }; - request(uuid, FileAction.Factory.makeRead(path, offset.get(), length), ref::fsResponseConsumer); + int offset = 0; + int length = (int) Math.min(BUFFER_SIZE, size - offset); + var ref = new ReaderCallback(callback, output, offset, length, path, size); + ref.requestRead(0, BUFFER_SIZE); } public DataPath getRootDirectory() { @@ -230,9 +214,9 @@ public void createDirectories(Path path, Consumer> o) { } private void writeLarge(Path path, byte[] data, Consumer> callback) { - byte[] buffer = new byte[1024]; + byte[] buffer = new byte[BUFFER_SIZE]; AtomicInteger offset = new AtomicInteger(0); - int length = Math.min(1024, data.length - offset.get()); + int length = Math.min(BUFFER_SIZE, data.length - offset.get()); System.arraycopy(data, 0, buffer, 0, length); var ref = new Object() { void fsResponseConsumer(FSResponse r) { @@ -255,7 +239,7 @@ void fsResponseConsumer(FSResponse r) { } private boolean writeNext(Path path, byte[] data, int offset, Consumer> callback) { - int length = Math.min(1024, data.length - offset); + int length = Math.min(BUFFER_SIZE, data.length - offset); if (length <= 0) { return false; } @@ -277,4 +261,44 @@ public static Type fromString(String type) { return UNKNOWN; } } + + private class ReaderCallback { + private final Consumer> callback; + private final byte[] output; + private final int length; + private final Path path; + private final long size; + private int offset; + + public ReaderCallback(Consumer> callback, byte[] output, int offset, int length, Path path, long size) { + this.callback = callback; + this.output = output; + this.offset = offset; + this.length = length; + this.path = path; + this.size = size; + } + + void fsResponseConsumer(FSResponse r) { + if (!r.success()) { + callback.accept(r); + return; + } + + int len = r.data().length; + System.arraycopy(r.data(), 0, output, offset, len); + + if (offset + len == size) { + callback.accept(new FSResponse<>(true, FileSystem.Status.SUCCESSFUL, output, "")); + return; + } + + requestRead(offset + len, BUFFER_SIZE); + } + + private void requestRead(long offset, int length) { + this.offset = (int) offset; + request(uuid, FileAction.Factory.makeRead(path, offset, (int) Math.min(length, size - offset)), this::fsResponseConsumer); + } + } } diff --git a/common/src/main/java/com/ultreon/devices/core/BiosApi.java b/common/src/main/java/com/ultreon/devices/core/BiosApi.java new file mode 100644 index 000000000..98f1e69f5 --- /dev/null +++ b/common/src/main/java/com/ultreon/devices/core/BiosApi.java @@ -0,0 +1,35 @@ +package com.ultreon.devices.core; + +import org.graalvm.polyglot.Value; + +import java.io.IOException; +import java.util.Map; + +public class BiosApi { + private final Laptop laptop; + private InventoryApi inventory; + private UiApi uiApi; + + public BiosApi(Laptop laptop) { + this.laptop = laptop; + + this.inventory = new InventoryApi(laptop); + this.uiApi = new UiApi(laptop); + } + + public boolean isWorldLess() { + return Laptop.isWorldLess(); + } + + public InventoryApi getInventoryApi() { + return inventory; + } + + public UiApi getUiApi() { + return uiApi; + } + + public PyProcess spawnProcess(Value modules, String[] command, Map env) throws IOException { + return laptop.spawnProcess(modules, command, env); + } +} diff --git a/common/src/main/java/com/ultreon/devices/core/BootImage.java b/common/src/main/java/com/ultreon/devices/core/BootImage.java new file mode 100644 index 000000000..922a34331 --- /dev/null +++ b/common/src/main/java/com/ultreon/devices/core/BootImage.java @@ -0,0 +1,26 @@ +package com.ultreon.devices.core; + +import com.ultreon.devices.core.io.drive.AbstractDrive; +import net.minecraft.world.level.storage.LevelResource; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.UUID; + +public class BootImage { + private final byte[] bytes; + + public BootImage(byte[] bytes) { + this.bytes = bytes; + } + + public void write(UUID uuid) throws IOException { + if (uuid == null) return; + Path file = AbstractDrive.getDrivePath(uuid); + Files.createDirectories(file.getParent()); + Files.write(file, bytes, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + } +} diff --git a/common/src/main/java/com/ultreon/devices/core/Ext2FS.java b/common/src/main/java/com/ultreon/devices/core/Ext2FS.java index e99e1264e..1bd0b1cee 100644 --- a/common/src/main/java/com/ultreon/devices/core/Ext2FS.java +++ b/common/src/main/java/com/ultreon/devices/core/Ext2FS.java @@ -1,6 +1,11 @@ package com.ultreon.devices.core; import com.google.common.base.Preconditions; +import com.ultreon.devices.Reference; +import com.ultreon.devices.core.io.drive.AbstractDrive; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.Resource; +import net.minecraft.server.packs.resources.ResourceManager; import net.minecraft.util.Unit; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -21,9 +26,12 @@ import java.nio.file.*; import java.util.Collections; import java.util.Iterator; +import java.util.Optional; +import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +@SuppressWarnings("t") public class Ext2FS implements FS { private final FileSystem fs; private final ConcurrentMap locks = new ConcurrentHashMap<>(); @@ -41,6 +49,17 @@ private Ext2FS(Ext2FileSystem fs) { }); } + public static Ext2FS loadBootImage(ResourceManager manager, String name, UUID uuid) throws FileSystemException { + Optional resource = manager.getResource(ResourceLocation.fromNamespaceAndPath(Reference.MOD_ID, "filesystems/" + name + ".ext2")); + if (resource.isEmpty()) return null; + try (var opened = resource.get().open()) { + Files.copy(opened, AbstractDrive.getDrivePath(uuid)); + return Ext2FS.open(AbstractDrive.getDrivePath(uuid)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + public static Ext2FS open(Path filePath) throws IOException, FileSystemException { return open(false, filePath); } @@ -160,7 +179,8 @@ private Ext2Directory getDirectoryAt(Path path) throws IOException { return (Ext2Directory) fsEntry.getDirectory(); } - private @Nullable Ext2Entry getFsEntry(Path path) throws IOException { + @Nullable + public Ext2Entry getFsEntry(Path path) throws IOException { if (!path.toString().startsWith("/")) path = Path.of("/" + path); if (!path.toString().startsWith("/")) path = Path.of("/" + path); if (path.getParent() == null) return (Ext2Entry) fs.getRootEntry(); @@ -168,6 +188,7 @@ private Ext2Directory getDirectoryAt(Path path) throws IOException { Ext2Directory root = (Ext2Directory) fs.getRootEntry().getDirectory(); for (Path s : path.getParent()) { Ext2Entry entry = (Ext2Entry) root.getEntry(s.toString()); + if (entry == null) return null; if (!entry.isDirectory()) return null; root = (Ext2Directory) entry.getDirectory(); if (root == null) return null; diff --git a/common/src/main/java/com/ultreon/devices/core/InventoryApi.java b/common/src/main/java/com/ultreon/devices/core/InventoryApi.java new file mode 100644 index 000000000..c80bfef4e --- /dev/null +++ b/common/src/main/java/com/ultreon/devices/core/InventoryApi.java @@ -0,0 +1,9 @@ +package com.ultreon.devices.core; + +public class InventoryApi { + private final Laptop laptop; + + public InventoryApi(Laptop laptop) { + this.laptop = laptop; + } +} diff --git a/common/src/main/java/com/ultreon/devices/core/Laptop.java b/common/src/main/java/com/ultreon/devices/core/Laptop.java index 9089780a1..a1cbabbd4 100644 --- a/common/src/main/java/com/ultreon/devices/core/Laptop.java +++ b/common/src/main/java/com/ultreon/devices/core/Laptop.java @@ -5,11 +5,13 @@ import com.mojang.blaze3d.vertex.PoseStack; import com.ultreon.devices.Devices; import com.ultreon.devices.api.ApplicationManager; +import com.ultreon.devices.api.DeviceAPI; +import com.ultreon.devices.api.app.*; import com.ultreon.devices.api.app.Dialog; import com.ultreon.devices.api.app.System; -import com.ultreon.devices.api.app.*; import com.ultreon.devices.api.app.component.Image; import com.ultreon.devices.api.io.Drive; +import com.ultreon.devices.api.io.FSResponse; import com.ultreon.devices.api.task.Callback; import com.ultreon.devices.api.task.Task; import com.ultreon.devices.api.task.TaskManager; @@ -19,6 +21,7 @@ import com.ultreon.devices.core.io.task.TaskGetMainDrive; import com.ultreon.devices.core.task.TaskInstallApp; import com.ultreon.devices.object.AppInfo; +import com.ultreon.devices.programs.activation.ActivationApp; import com.ultreon.devices.programs.system.DiagnosticsApp; import com.ultreon.devices.programs.system.DisplayResolution; import com.ultreon.devices.programs.system.PredefinedResolution; @@ -47,19 +50,35 @@ import net.minecraft.network.chat.Style; import net.minecraft.resources.ResourceLocation; import net.minecraft.sounds.SoundEvents; +import net.minecraft.util.Unit; +import org.apache.commons.compress.utils.SeekableInMemoryByteChannel; +import org.graalvm.polyglot.*; +import org.graalvm.polyglot.io.FileSystem; +import org.graalvm.polyglot.io.IOAccess; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.awt.*; import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.io.PrintStream; -import java.nio.file.AccessDeniedException; -import java.nio.file.Path; -import java.util.List; +import java.net.URI; +import java.nio.channels.SeekableByteChannel; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.*; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.FileTime; +import java.nio.file.attribute.UserPrincipal; import java.util.*; +import java.util.List; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ExecutionException; import java.util.function.Consumer; +import static java.lang.System.currentTimeMillis; + /// Laptop GUI class. /// /// @author MrCrayfish, Qboi123 @@ -71,8 +90,16 @@ public class Laptop extends Screen implements System { private static Font font; private static final ResourceLocation LAPTOP_GUI = ResourceLocation.fromNamespaceAndPath(Devices.MOD_ID, "textures/gui/laptop.png"); private static final List APPLICATIONS = new ArrayList<>(); + private static final int ACTIVATE_RETRY = 60; private static boolean worldLess; private static Laptop instance; + private final Context codeContext; + private final CompletableFuture codeThread; + private final LaptopFileSystem fileSystem; + private boolean registered; + private UUID license = null; + private int retryActivate = seconds2ticks(ACTIVATE_RETRY); + private ActivationApp registerApp; private Double dragWindowFromX; private Double dragWindowFromY; private final VideoInfo videoInfo; @@ -81,6 +108,7 @@ public class Laptop extends Screen implements System { private Window systemDialogWindow = null; private static boolean loaded; private Bios bios; + private final BiosApi biosApi = new BiosApi(this); @PlatformOnly("fabric") public static List getApplicationsForFabric() { @@ -140,6 +168,10 @@ public Laptop(ComputerBlockEntity laptop, boolean worldLess) { this.appData = laptop.getApplicationData(); this.systemData = laptop.getSystemData(); + if (this.systemData.contains("License")) { + license = this.systemData.getUUID("License"); + } + CompoundTag videoInfoData = this.systemData.getCompound("videoInfo"); this.videoInfo = new VideoInfo(videoInfoData); @@ -186,6 +218,57 @@ public boolean add(Window window) { // World-less flag. Laptop.worldLess = worldLess; + + Context.Builder js = Context.newBuilder("python") + .environment("OSTYPE", "MineOS") + .environment("PROCESSOR_ARCHITECTURE", "AMD64") + .environment("PYTHON_PLATFORM", "unix") + .environment("USER", "root") + .environment("SHELL", "/bin/shell.py") + .environment("LANG", "en_US.UTF-8") + .environment("HOME", "/root") + .environment("TERM", "shell"); + this.fileSystem = new LaptopFileSystem(); + js.allowIO(IOAccess.newBuilder().fileSystem(fileSystem).build()); + js.out(java.lang.System.out); + js.in(java.lang.System.in); + js.err(java.lang.System.err); + js.allowCreateThread(false); + js.allowCreateProcess(false); + js.allowNativeAccess(false); + js.allowEnvironmentAccess(EnvironmentAccess.NONE); + js.allowHostAccess(HostAccess.ISOLATED); + js.allowHostClassLoading(false); + js.allowValueSharing(false); + js.useSystemExit(false); + js.allowPolyglotAccess(PolyglotAccess.NONE); + codeContext = js.build(); + + codeThread = createCodeThread(); + } + + private CompletableFuture createCodeThread() { + CompletableFuture future = new CompletableFuture<>(); + getOrLoadMainDrive((drive, success) -> { + if (!success) { + future.completeExceptionally(new IOException("Failed to load main drive")); + return; + } + future.complete(drive); + }); + + return future.thenApply((drive) -> { + if (drive == null) { + future.completeExceptionally(new IOException("Failed to load main drive")); + return null; + } + + Thread codeThread = new Thread(this::initialize); + + codeThread.setDaemon(false); + codeThread.start(); + return codeThread; + }); } private Bios determineBios(ComputerBlockEntity laptop) { @@ -326,6 +409,12 @@ private static int getDeviceHeight() { @Override public void removed() { + try { + codeContext.close(true); + } catch (Exception e) { + // Ignore + } + /* Close all windows and sendTask application data */ for (int i = 0; i < windows.size(); i++) { Window window = windows.get(i); @@ -398,12 +487,31 @@ public void tick() { } } + if (!isActivated()) { + if (retryActivate >= seconds2ticks(ACTIVATE_RETRY)) { + retryActivate = 0; + Devices.LOGGER.info("System isn't activated, showing activate window."); + setSystemDialog(new ActivationApp()); + } else { + retryActivate++; + } + } + FileBrowser.refreshList = false; } catch (Exception e) { bsod(e); } } + public void showActivateWindow() { + Devices.LOGGER.info("System isn't activated, showing activate window."); + setSystemDialog(new ActivationApp()); + } + + private int seconds2ticks(int seconds) { + return seconds * 20; + } + @Override public void render(final @NotNull GuiGraphics graphics, final int mouseX, final int mouseY, float partialTicks) { super.renderBackground(graphics, mouseX, mouseY, partialTicks); @@ -669,6 +777,54 @@ public void requestPermission(PermissionRequest permissionRequest, Consumer env) throws IOException { + String s = command[0]; + String[] args = new String[command.length - 1]; + java.lang.System.arraycopy(command, 1, args, 0, command.length - 1); + return new PyProcess(fileSystem, modules, s, args, env); + } + private static final class BSOD { private final Throwable throwable; @@ -1376,4 +1532,368 @@ public Tag serialize() { return a; } } + + private record FileInfoDirectoryStream(FSResponse> voidFSResponse) implements DirectoryStream { + @Override + public void close() throws IOException { + + } + + @Override + public @NotNull Iterator iterator() { + return new Iterator<>() { + int index = 0; + + @Override + public boolean hasNext() { + return index < voidFSResponse.data().size(); + } + + @Override + public Path next() { + FileInfo info = voidFSResponse.data().get(index); + index++; + return info.getPath(); + } + }; + } + } + + public static class LaptopFileSystem implements FileSystem { + private final FileSystem delegate = FileSystem.newDefaultFileSystem(); + private Path currentWorkingDirectory = Path.of("/"); + + @Override + public Path parsePath(URI uri) { + return delegate.parsePath(uri); + } + + @Override + public Path parsePath(String path) { + return delegate.parsePath(path); + } + + @Override + public void checkAccess(Path path, Set modes, LinkOption... linkOptions) throws IOException { + if (isInternal(path)) { + try { + delegate.checkAccess(path, modes, linkOptions); + } catch (IOException e) { + throw e; + } + return; + } + CompletableFuture> future = new CompletableFuture<>(); + mainDrive.exists(path, future::complete); + try { + FSResponse booleanFSResponse = future.get(); + if (!booleanFSResponse.success()) { + throw new IOException(booleanFSResponse.message()); + } + } catch (InterruptedException | ExecutionException e) { + throw new IOException(e); + } catch (IOException e) { + throw e; + } + + } + + @Override + public void createDirectory(Path path, FileAttribute... attrs) throws IOException { + if (isInternal(path)) { + delegate.createDirectory(path, attrs); + return; + } CompletableFuture> future = new CompletableFuture<>(); + + path = path.isAbsolute() ? path : currentWorkingDirectory.resolve(path); + + if (path.equals(Path.of("/"))) { + throw new FileAlreadyExistsException("Cannot create root directory"); + } + Path fileName = path.getFileName(); + if (fileName == null) { + throw new IOException("Failed to create directory"); + } + mainDrive.createDirectory(path.getParent(), fileName.toString(), future::complete); + try { + FSResponse voidFSResponse = future.get(); + if (!voidFSResponse.success()) { + throw new IOException(voidFSResponse.message()); + } + + if (voidFSResponse.data() == null) { + throw new IOException("Failed to create directory"); + } + } catch (InterruptedException | ExecutionException e) { + throw new IOException(e); + } catch (IOException e) { + throw e; + } + } + + @Override + public void delete(Path path) throws IOException { + if (isInternal(path)) { + delegate.delete(path); + return; + } + path = path.isAbsolute() ? path : currentWorkingDirectory.resolve(path); + + CompletableFuture> future = new CompletableFuture<>(); + mainDrive.delete(path, future::complete); + try { + FSResponse voidFSResponse = future.get(); + if (!voidFSResponse.success()) { + throw new IOException(voidFSResponse.message()); + } + } catch (InterruptedException | ExecutionException e) { + throw new IOException(e); + } catch (IOException e) { + throw e; + } + } + + @Override + public SeekableByteChannel newByteChannel(Path path, Set options, FileAttribute... attrs) throws IOException { + if (isInternal(path)) { + return delegate.newByteChannel(path, options, attrs); + } + + path = path.isAbsolute() ? path : currentWorkingDirectory.resolve(path); + CompletableFuture> future = new CompletableFuture<>(); + mainDrive.read(path, future::complete); + try { + FSResponse voidFSResponse = future.get(); + if (!voidFSResponse.success()) { + throw new IOException(voidFSResponse.message()); + } + + return new SeekableInMemoryByteChannel(voidFSResponse.data()); + } catch (InterruptedException | ExecutionException e) { + throw new IOException(e); + } catch (IOException e) { + throw e; + } + } + + private static boolean isInternal(Path path) { + if (path.startsWith(Path.of(switch (java.lang.System.getProperty("os.name")) { + case "Linux" -> "/home/" + java.lang.System.getProperty("user.name") + "/.cache/org.graalvm.polyglot"; + case "Mac OS X" -> "/Users/" + java.lang.System.getProperty("user.name") + "/Library/Caches/org.graalvm.polyglot"; + case "Windows" -> "/Users/" + java.lang.System.getProperty("user.name") + "/AppData/Local/org.graalvm.polyglot"; + default -> throw new IllegalStateException(); + }))) { + return true; + } + return path.toString().equals("/lib") || path.toString().matches("^/lib/(python|graalpy)\\d+(\\.\\d+)+(/.*|$)") || path.toString().matches("<.*>"); + } + + @Override + public DirectoryStream newDirectoryStream(Path path, DirectoryStream.Filter filter) throws IOException { + if (isInternal(path)) { + return delegate.newDirectoryStream(path, filter); + } + path = path.isAbsolute() ? path : currentWorkingDirectory.resolve(path); + CompletableFuture>> future = new CompletableFuture<>(); + mainDrive.list(path, future::complete); + try { + FSResponse> voidFSResponse = future.get(); + if (!voidFSResponse.success()) { + throw new IOException(voidFSResponse.message()); + } + + return new FileInfoDirectoryStream(voidFSResponse); + } catch (InterruptedException | ExecutionException e) { + throw new IOException(e); + } catch (IOException e) { + throw e; + } + } + + @Override + public Path toAbsolutePath(Path path) { + return path.toAbsolutePath(); + } + + @Override + public Path toRealPath(Path path, LinkOption... linkOptions) throws IOException { + return path.toRealPath(linkOptions); + } + + @Override + public Map readAttributes(Path path, String attributes, LinkOption... options) throws IOException { + if (isInternal(path)) { + Map stringObjectMap = delegate.readAttributes(path, attributes, options); + return stringObjectMap; + } + + path = path.isAbsolute() ? path : currentWorkingDirectory.resolve(path); + CompletableFuture> future = new CompletableFuture<>(); + mainDrive.info(path, future::complete); + try { + FSResponse voidFSResponse = future.get(); + if (!voidFSResponse.success()) { + throw new IOException(voidFSResponse.message()); + } + + FileInfo info = voidFSResponse.data(); + if (info == null) { + throw new IOException("Failed to get file info"); + } + + Map map = new HashMap<>(); + if (attributes.startsWith("unix:")) { + attributes = attributes.substring("unix:".length()); + } else if (attributes.startsWith("basic:")) { + attributes = attributes.substring("basic:".length()); + } else { + throw new IllegalArgumentException("Unsupported attributes: " + attributes); + } + for (@NotNull String entry : attributes.split(",")) { + switch (entry) { + case "size" -> map.put("size", info.getSize()); + case "isDirectory" -> map.put("isDirectory", info.isFolder()); + case "isRegularFile" -> map.put("isRegularFile", info.isFile()); + case "isSymbolicLink" -> map.put("isSymbolicLink", info.isSymbolicLink()); + case "uid" -> map.put("uid", info.getUid()); + case "gid" -> map.put("gid", info.getGid()); + case "owner" -> map.put("owner", (UserPrincipal) () -> "user"); + case "permissions" -> map.put("permissions", info.getMode()); + case "creationTime" -> map.put("creationTime", FileTime.fromMillis(info.getCreationTime())); + case "lastAccessed" -> map.put("lastAccessed", FileTime.fromMillis(info.getLastAccessed())); + case "lastAccessedTime" -> map.put("lastAccessedTime", FileTime.fromMillis(info.getLastAccessed())); + case "lastAccessTime" -> map.put("lastAccessTime", FileTime.fromMillis(info.getLastAccessed())); + case "lastModified" -> map.put("lastModified", FileTime.fromMillis(info.getLastModified())); + case "lastModifiedTime" -> map.put("lastModifiedTime", FileTime.fromMillis(info.getLastModified())); + case "createdTime" -> map.put("createdTime", FileTime.fromMillis(info.getCreationTime())); + case "inode" -> map.put("inode", info.getInode()); + case "fileKey" -> map.put("fileKey", info.getFileKey()); + case "ino" -> map.put("ino", info.getIno()); + case "rdev" -> map.put("rdev", info.getDev()); + case "atime" -> map.put("atime", FileTime.fromMillis(currentTimeMillis())); + case "mtime" -> map.put("mtime", FileTime.fromMillis(currentTimeMillis())); + case "ctime" -> map.put("ctime", FileTime.fromMillis(currentTimeMillis())); + case "dev" -> map.put("dev", info.getDev()); + case "nlink" -> map.put("nlink", info.getNlink()); + case "mode" -> map.put("mode", info.getMode()); + default -> + throw new IOException("Unknown attribute: " + entry); + } + } + return map; + } catch (InterruptedException | ExecutionException e) { + throw new IOException(e); + } catch (IOException e) { + throw e; + } + } + + @Override + public void copy(Path source, Path target, CopyOption... options) throws IOException { + if (isInternal(source) || isInternal(target)) { + throw new IOException("Cannot copy internal files"); + } + + boolean overwrite = false; + for (CopyOption option : options) { + switch (option) { + case StandardCopyOption.REPLACE_EXISTING -> overwrite = true; + case StandardCopyOption.COPY_ATTRIBUTES -> { + // Ignore + } + case StandardCopyOption.ATOMIC_MOVE -> throw new IOException("Atomic move not supported"); + case null, default -> throw new IOException("Option not supported: " + option); + } + } + + CompletableFuture> future = new CompletableFuture<>(); + source = source.isAbsolute() ? source : currentWorkingDirectory.resolve(source); + target = target.isAbsolute() ? target : currentWorkingDirectory.resolve(target); + mainDrive.copy(source, target, overwrite, future::complete); + try { + FSResponse voidFSResponse = future.get(); + if (!voidFSResponse.success()) { + throw new IOException(voidFSResponse.message()); + } + } catch (InterruptedException | ExecutionException e) { + throw new IOException(e); + } catch (IOException e) { + throw e; + } + } + + @Override + public void move(Path source, Path target, CopyOption... options) throws IOException { + if (isInternal(source) || isInternal(target)) { + throw new IOException("Cannot move internal files"); + } + + boolean overwrite = false; + for (CopyOption option : options) { + switch (option) { + case StandardCopyOption.REPLACE_EXISTING -> overwrite = true; + case StandardCopyOption.COPY_ATTRIBUTES -> { + // Ignore + } + case StandardCopyOption.ATOMIC_MOVE -> throw new IOException("Atomic move not supported"); + case null, default -> throw new IOException("Option not supported: " + option); + } + } + + CompletableFuture> future = new CompletableFuture<>(); + source = source.isAbsolute() ? source : currentWorkingDirectory.resolve(source); + target = target.isAbsolute() ? target : currentWorkingDirectory.resolve(target); + mainDrive.move(source, target, overwrite, future::complete); + try { + FSResponse voidFSResponse = future.get(); + if (!voidFSResponse.success()) { + throw new IOException(voidFSResponse.message()); + } + } catch (InterruptedException | ExecutionException e) { + throw new IOException(e); + } + } + + @Override + public void createSymbolicLink(Path link, Path target, FileAttribute... attrs) throws IOException { + throw new IOException("Symbolic links not supported"); + } + + @Override + public void createLink(Path link, Path existing) throws IOException { + throw new IOException("Hard links not supported"); + } + + @Override + public void setCurrentWorkingDirectory(Path currentWorkingDirectory) { + this.delegate.setCurrentWorkingDirectory(currentWorkingDirectory); + + this.currentWorkingDirectory = currentWorkingDirectory; + } + + @Override + public String getSeparator() { + return "/"; + } + + @Override + public String getPathSeparator() { + return ":"; + } + + @Override + public Path getTempDirectory() { + return Path.of("/tmp"); + } + + @Override + public Charset getEncoding(Path path) { + return StandardCharsets.UTF_8; + } + + @Override + public Path readSymbolicLink(Path link) throws IOException { + throw new IOException("Symbolic links not supported"); + } + } } diff --git a/common/src/main/java/com/ultreon/devices/core/PyProcess.java b/common/src/main/java/com/ultreon/devices/core/PyProcess.java new file mode 100644 index 000000000..960d722fd --- /dev/null +++ b/common/src/main/java/com/ultreon/devices/core/PyProcess.java @@ -0,0 +1,78 @@ +package com.ultreon.devices.core; + +import org.apache.commons.io.input.NullInputStream; +import org.apache.commons.io.output.NullOutputStream; +import org.graalvm.polyglot.Context; +import org.graalvm.polyglot.PolyglotAccess; +import org.graalvm.polyglot.Source; +import org.graalvm.polyglot.Value; +import org.graalvm.polyglot.io.IOAccess; +import org.graalvm.polyglot.io.ProcessHandler; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.SeekableByteChannel; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.Collections; +import java.util.Map; +import java.util.function.IntConsumer; + +public class PyProcess extends Thread { + private final Laptop.LaptopFileSystem fs; + private final Value context; + private final String path; + private final String[] args; + private final Map env; + private IntConsumer onExit; + + public PyProcess(Laptop.LaptopFileSystem fs, Value modules, String path, String[] args, Map env) throws IOException { + this.fs = fs; + this.context = modules; + this.path = path; + this.args = args; + this.env = env; + } + + public void setOnExit(IntConsumer handler) { + this.onExit = handler; + } + + @Override + public void run() { + try (Context context = Context.newBuilder("python") + .environment(env) + .allowCreateProcess(false) + .allowCreateThread(false) + .allowIO(IOAccess.newBuilder().fileSystem(fs).build()) + .allowValueSharing(true) + .allowNativeAccess(false) + .allowPolyglotAccess(PolyglotAccess.NONE) + .in(NullInputStream.INSTANCE) + .out(NullOutputStream.INSTANCE) + .err(NullOutputStream.INSTANCE) + .allowHostClassLoading(false) + .useSystemExit(false) + .build()) { + + context.enter(); + + try (SeekableByteChannel seekableByteChannel = fs.newByteChannel(Path.of(path), Collections.emptySet())) { + long size = seekableByteChannel.size(); + ByteBuffer buffer = ByteBuffer.allocate((int) size); + seekableByteChannel.read(buffer); + buffer.flip(); + Value python = context.eval(Source.newBuilder("python", new String(buffer.array(), StandardCharsets.UTF_8), path).encoding(StandardCharsets.UTF_8).build()); + Value execute = python.getMember("main").execute("main", args); + int anInt = execute.asInt(); + if (anInt != 0) { + onExit.accept(anInt); + } + } catch (IOException e) { + onExit.accept(1); + } + + context.leave(); + } + } +} diff --git a/common/src/main/java/com/ultreon/devices/core/UiApi.java b/common/src/main/java/com/ultreon/devices/core/UiApi.java new file mode 100644 index 000000000..dd973e64c --- /dev/null +++ b/common/src/main/java/com/ultreon/devices/core/UiApi.java @@ -0,0 +1,9 @@ +package com.ultreon.devices.core; + +public class UiApi { + private final Laptop laptop; + + public UiApi(Laptop laptop) { + this.laptop = laptop; + } +} diff --git a/common/src/main/java/com/ultreon/devices/core/ValueSharingExample.java b/common/src/main/java/com/ultreon/devices/core/ValueSharingExample.java new file mode 100644 index 000000000..2251b5eba --- /dev/null +++ b/common/src/main/java/com/ultreon/devices/core/ValueSharingExample.java @@ -0,0 +1,50 @@ +package com.ultreon.devices.core; + +import org.graalvm.polyglot.*; +import org.graalvm.polyglot.io.MessageTransport; + +public class ValueSharingExample { + public static void main(String[] args) { + Engine engine = Engine.newBuilder("python") + .build(); + + try (Context context1 = Context.newBuilder() + .engine(engine) + .allowValueSharing(true) + .allowPolyglotAccess(PolyglotAccess.newBuilder().allowBindingsAccess("python").build()) + .allowHostAccess(HostAccess.ALL) // ✅ Required for polyglot imports/exports + .build(); + Context context2 = Context.newBuilder() + .engine(engine) + .allowValueSharing(true) + .allowPolyglotAccess(PolyglotAccess.newBuilder().allowBindingsAccess("python").build()) + .allowHostAccess(HostAccess.ALL) // ✅ Required for polyglot imports/exports + .build(); + ) { + // Python imports the shared value and modifies it + context1.eval("python", """ + import sys + sys.exit = lambda x: x + """); + + context2.eval("python", """ + import sys + print(sys.exit) + sys.exit = 2 + """); + + context1.eval("python", """ + import sys + print(sys.exit) + """); + + // Retrieve updated value from Java + Value updatedValue = context1.getBindings("python").getMember("shared_value"); + + // Print the updated value in Java + System.out.println("Updated in Java: " + updatedValue.getMember("msg").asString()); + } + + engine.close(); + } +} \ No newline at end of file diff --git a/common/src/main/java/com/ultreon/devices/core/io/drive/AbstractDrive.java b/common/src/main/java/com/ultreon/devices/core/io/drive/AbstractDrive.java index e1f8bd827..6e6ed4e56 100644 --- a/common/src/main/java/com/ultreon/devices/core/io/drive/AbstractDrive.java +++ b/common/src/main/java/com/ultreon/devices/core/io/drive/AbstractDrive.java @@ -15,6 +15,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jnode.fs.FileSystemException; +import org.jnode.fs.ext2.Ext2Entry; import java.io.IOException; import java.io.InputStream; @@ -55,7 +56,7 @@ public abstract class AbstractDrive implements FS { Path resolve = Devices.getServer().getWorldPath(LevelResource.ROOT).resolve("data/devices/drives/" + uuid + ".ext2"); if (Files.notExists(resolve)) { if (Files.notExists(resolve.getParent())) Files.createDirectories(resolve.getParent()); - this.setFs(Ext2FS.format(resolve, 1L)); + this.setFs(Ext2FS.loadBootImage(Devices.getServer().getResourceManager(), "main", uuid)); this.setup(); } else this.setFs(Ext2FS.open(resolve)); } catch (IOException | FileSystemException e) { @@ -91,7 +92,7 @@ public AbstractDrive(String name, UUID uuid) { Path resolve = Devices.getServer().getWorldPath(LevelResource.ROOT).resolve("data/devices/drives/" + uuid + ".ext2"); if (Files.notExists(resolve)) { if (Files.notExists(resolve.getParent())) Files.createDirectories(resolve.getParent()); - this.setFs(Ext2FS.format(resolve, 16 * 1024 * 1024)); + this.setFs(Ext2FS.loadBootImage(Devices.getServer().getResourceManager(), "main", uuid)); this.setup(); } else this.setFs(Ext2FS.open(resolve)); } catch (IOException | FileSystemException e) { @@ -224,6 +225,20 @@ private CompoundTag info(Path path) throws IOException { data.putString("path", path.toString()); data.putBoolean("protected", getFs().isReadOnly(path)); data.putBoolean("folder", getFs().isFolder(path)); + Ext2Entry fsEntry = getFs().getFsEntry(path); + data.putLong("ino", fsEntry == null ? 0 : fsEntry.getINode().getINodeNr()); + data.putLong("inode", fsEntry == null ? 0 : fsEntry.getINode().getINodeNr()); + data.putLong("lastModified", fsEntry == null ? 0L : fsEntry.getINode().getMtime()); + data.putLong("lastAccessed", fsEntry == null ? 0L : fsEntry.getINode().getAtime()); + data.putLong("creationTime", fsEntry == null ? 0L : fsEntry.getINode().getCtime()); + data.putBoolean("exists", fsEntry != null); + data.putShort("mode", fsEntry == null ? 0 : (short) fsEntry.getINode().getMode()); + data.putLong("dev", fsEntry == null ? 0L : uuid.hashCode()); + data.putByte("nlink", (byte) (fsEntry == null ? 0 : 1)); + data.putInt("uid", fsEntry == null ? 0 : fsEntry.getINode().getUid()); + data.putInt("gid", fsEntry == null ? 0 : fsEntry.getINode().getGid()); + data.putLong("rdev", fsEntry == null ? 0L : uuid.hashCode()); + data.putBoolean("isSymlink", false); data.putLong("size", getFs().size(path)); return data; } diff --git a/common/src/main/java/com/ultreon/devices/programs/activation/ActivationApp.java b/common/src/main/java/com/ultreon/devices/programs/activation/ActivationApp.java new file mode 100644 index 000000000..179beff0f --- /dev/null +++ b/common/src/main/java/com/ultreon/devices/programs/activation/ActivationApp.java @@ -0,0 +1,113 @@ +package com.ultreon.devices.programs.activation; + +import com.ultreon.devices.api.app.Dialog; +import com.ultreon.devices.api.app.Icons; +import com.ultreon.devices.api.app.component.Button; +import com.ultreon.devices.api.app.component.Label; +import com.ultreon.devices.api.app.component.TextField; +import com.ultreon.devices.api.task.TaskManager; +import com.ultreon.devices.core.Laptop; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.nbt.CompoundTag; +import org.jetbrains.annotations.Nullable; + +import java.util.UUID; + +/** + * Codename: Apr1l + */ +public class ActivationApp extends Dialog { + private TextField part1; + private TextField part2; + private TextField part3; + private TextField part4; + private TextField part5; + private Label sep1; + private Label sep2; + private Label sep3; + private Label sep4; + private Button registerBtn; + + public ActivationApp() { + width = 10 + length(8) + 9 + length(4) + 9 + length(4) + 9 + length(4) + 9 + length(12) + 10; + height = 68; + } + + @Override + public void init(@Nullable CompoundTag intent) { + super.init(intent); + + defaultLayout.width = 10 + length(8) + 9 + length(4) + 9 + length(4) + 9 + length(4) + 9 + length(12) + 10; + defaultLayout.height = 68; + defaultLayout.setTitle("Activate"); + + // 00000000-0000-0000-0000-000000000000 + Label description = new Label("Enter product key to activate:", 10, 10); + int i = 10; + part1 = new TextField(10, 25, length(8)); + part1.setPlaceholder("00000000"); + part2 = new TextField(10 + length(8) + i, 25, length(4)); + part2.setPlaceholder("0000"); + part3 = new TextField(10 + length(8) + i + length(4) + i, 25, length(4)); + part3.setPlaceholder("0000"); + part4 = new TextField(10 + length(8) + i + length(4) + i + length(4) + i, 25, length(4)); + part4.setPlaceholder("0000"); + part5 = new TextField(10 + length(8) + i + length(4) + i + length(4) + i + length(4) + i, 25, length(12)); + part5.setPlaceholder("000000000000"); + sep1 = new Label("-", 10 + length(8) + 2, 29); + sep2 = new Label("-", 10 + length(8) + i + length(4) + 2, 29); + sep3 = new Label("-", 10 + length(8) + i + length(4) + i + length(4) + 2, 29); + sep4 = new Label("-", 10 + length(8) + i + length(4) + i + length(4) + i + length(4) + 2, 29); + registerBtn = new Button(10 + length(8) + i + length(4) + i + length(4) + i + length(4) + i + length(12) - 70, 45, 70, 18, "Activate"); + registerBtn.setIcon(Icons.KEY); + registerBtn.setClickListener((mouseX, mouseY, mouseButton) -> { + UUID license = getLicense(); + if (license == null) { + openDialog(new Dialog.Message("Invalid license.")); + return; + } + TaskActivateMineOS activateTask = new TaskActivateMineOS(); + activateTask.setCallback((nbt, success) -> { + if (success) { + getWindow().close(); + } else { + openDialog(new Dialog.Message("Incorrect license.")); + } + }); + TaskManager.sendTask(activateTask); + }); + + super.addComponent(description); + super.addComponent(part1); + super.addComponent(part2); + super.addComponent(part3); + super.addComponent(part4); + super.addComponent(part5); + super.addComponent(sep1); + super.addComponent(sep2); + super.addComponent(sep3); + super.addComponent(sep4); + super.addComponent(registerBtn); + } + + @Override + public void render(GuiGraphics graphics, Laptop laptop, Minecraft mc, int x, int y, int mouseX, int mouseY, boolean active, float partialTicks) { setLayout(defaultLayout); + defaultLayout.width = width; + defaultLayout.height = height; + + super.render(graphics, laptop, mc, x, y, mouseX, mouseY, active, partialTicks); + } + + private UUID getLicense() { + try { + return UUID.fromString(part1 + "-" + part2 + "-" + part3 + "-" + part4 + "-" + part5); + } catch (Exception e) { + return null; + } + } + + private int length(int i) { + return 6 * i - 1 + 10; + } +} diff --git a/common/src/main/java/com/ultreon/devices/programs/activation/LicenseManager.java b/common/src/main/java/com/ultreon/devices/programs/activation/LicenseManager.java new file mode 100644 index 000000000..cfcd718b2 --- /dev/null +++ b/common/src/main/java/com/ultreon/devices/programs/activation/LicenseManager.java @@ -0,0 +1,11 @@ +package com.ultreon.devices.programs.activation; + +import com.ultreon.devices.core.Laptop; +import dev.architectury.utils.EnvExecutor; + +public class LicenseManager { + + public static boolean isActivated() { + return EnvExecutor.getEnvSpecific(() -> () -> Laptop.getInstance().isActivated(), () -> () -> false); + } +} diff --git a/common/src/main/java/com/ultreon/devices/programs/activation/TaskActivateMineOS.java b/common/src/main/java/com/ultreon/devices/programs/activation/TaskActivateMineOS.java new file mode 100644 index 000000000..e719c5d36 --- /dev/null +++ b/common/src/main/java/com/ultreon/devices/programs/activation/TaskActivateMineOS.java @@ -0,0 +1,44 @@ +package com.ultreon.devices.programs.activation; + +import com.ultreon.devices.api.task.Task; +import com.ultreon.devices.core.Laptop; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; + +import java.util.UUID; + +public class TaskActivateMineOS extends Task { + private String name; + private UUID license; + + public TaskActivateMineOS() { + super("activate_mine_os"); + } + + public TaskActivateMineOS(UUID license) { + this(); + this.license = license; + } + + @Override + public void prepareRequest(CompoundTag nbt) { + nbt.putUUID("License", this.license); + } + + @Override + public void processRequest(CompoundTag nbt, Level level, Player player) { + if (Laptop.getInstance().activate(nbt.getUUID("License"))) { + this.setSuccessful(); + } + } + + @Override + public void prepareResponse(CompoundTag nbt) { + } + + @Override + public void processResponse(CompoundTag nbt) { + } + +} diff --git a/common/src/main/java/com/ultreon/devices/programs/system/SettingsApp.java b/common/src/main/java/com/ultreon/devices/programs/system/SettingsApp.java index 0b3c439a6..c3112fe3e 100644 --- a/common/src/main/java/com/ultreon/devices/programs/system/SettingsApp.java +++ b/common/src/main/java/com/ultreon/devices/programs/system/SettingsApp.java @@ -18,10 +18,13 @@ import com.ultreon.devices.core.Laptop; import com.ultreon.devices.object.AppInfo; import com.ultreon.devices.object.TrayItem; +import com.ultreon.devices.programs.activation.LicenseManager; import com.ultreon.devices.programs.system.component.Palette; import com.ultreon.devices.programs.system.object.ColorScheme; import com.ultreon.devices.programs.system.object.ColorSchemePresetRegistry; import com.ultreon.devices.programs.system.object.Preset; +import dev.architectury.utils.Env; +import dev.architectury.utils.EnvExecutor; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.nbt.CompoundTag; @@ -95,28 +98,45 @@ private Menu addMainLayout() { this.layoutColorSchemes = createColorSchemesLayout(); this.layoutGeneral = createGeneralLayout(); - Button buttonColorScheme = new Button(5, 26 + 20 + 4, "Personalise", Icons.EDIT); + if (!LicenseManager.isActivated()) { + Button buttonActivate = new Button(5, 26 + 20 + 4, "Activate", Icons.UNLOCK); + buttonActivate.setSize(90, 20); + buttonActivate.setToolTip("Activate", "Activate your license"); + buttonActivate.setClickListener((mouseX, mouseY, mouseButton) -> { + EnvExecutor.runInEnv(Env.CLIENT, () -> () -> { + Laptop laptop = getLaptop(); + if (laptop != null) { + laptop.showActivateWindow(); + } + }); + }); + layoutMain.addComponent(buttonActivate); + } + + Button buttonColorScheme = new Button(5, 26 + 26 + 20 + 4, "Personalise", Icons.EDIT); buttonColorScheme.setSize(90, 20); buttonColorScheme.setToolTip("Personalise", "Change the wallpaper, UI colors, and more!"); buttonColorScheme.setClickListener((mouseX, mouseY, mouseButton) -> { if (mouseButton == 0) { + if (checkLicense()) return; showMenu(layoutPersonalise); + } }); - layoutMain.addComponent(buttonColorScheme); - Button buttonColorSchemes = new Button(5, 26 + 26 + 20 + 4, "Themes", Icons.WRENCH); + Button buttonColorSchemes = new Button(5, 26 + 26 + 26 + 20 + 4, "Themes", Icons.WRENCH); buttonColorSchemes.setSize(90, 20); buttonColorSchemes.setToolTip("Color Schemes", "Change the color scheme using presets or choose a custom one."); buttonColorSchemes.setClickListener((mouseX, mouseY, mouseButton) -> { if (mouseButton == 0) { + if (checkLicense()) return; showMenu(layoutColorSchemes); } }); layoutMain.addComponent(buttonColorSchemes); - Button buttonGeneral = new Button(5, 26 + 26 + 26 + 20 + 4, "Advanced", Icons.WRENCH); + Button buttonGeneral = new Button(5, 26 + 26 + 26 + 26 + 20 + 4, "Advanced", Icons.WRENCH); buttonGeneral.setSize(90, 20); buttonGeneral.setToolTip("General", "General settings."); buttonGeneral.setClickListener((mouseX, mouseY, mouseButton) -> { @@ -129,6 +149,24 @@ private Menu addMainLayout() { return layoutMain; } + private boolean checkLicense() { + if (!LicenseManager.isActivated()) { + Dialog.Confirmation confirmation = new Dialog.Confirmation("You need to buy a license before using this.\nDo you want to activate now?"); + openDialog(confirmation); + confirmation.setPositiveListener((mouseX1, mouseY1, mouseButton1) -> { + EnvExecutor.runInEnv(Env.CLIENT, () -> () -> { + Laptop laptop = getLaptop(); + if (laptop != null) { + laptop.showActivateWindow(); + } + }); + }); + + return true; + } + return false; + } + @NotNull private Button createAboutButton(Menu layoutMain) { Button aboutButton = new Button(5, 26, "About", Icons.INFO); diff --git a/common/src/main/java/com/ultreon/devices/programs/system/component/FileInfo.java b/common/src/main/java/com/ultreon/devices/programs/system/component/FileInfo.java index 91c9cc471..dda309c94 100644 --- a/common/src/main/java/com/ultreon/devices/programs/system/component/FileInfo.java +++ b/common/src/main/java/com/ultreon/devices/programs/system/component/FileInfo.java @@ -25,6 +25,11 @@ public final class FileInfo { public static final Comparator SORT_BY_NAME = Comparator.comparing(a -> a.path.getFileName().toString()); public static final Comparator SORT_BY_TYPE = Comparator.comparing(a -> a.type); + private final long dev; + private final int nlink; + private final int uid; + private final int gid; + private final short mode; private Path path; private final DataPath dataPath; private final FSEntryType type; @@ -32,15 +37,31 @@ public final class FileInfo { private Drive drive; private boolean invalid; private long size; + private long inode; + private long ino; private long lastModified; private long lastAccessed; private long creationTime; + private int fileKey; + private boolean isSymbolicLink; /// Creates a new file info object - public FileInfo(Drive drive, Path path, FSEntryType type, long size, boolean protectedFile) { + public FileInfo(Drive drive, Path path, FSEntryType type, long size, long ino, long lastModified, long lastAccessed, long creationTime, long inode, long dev, int nlink, int uid, int gid, boolean protectedFile, short mode, boolean isSymbolicLink) { this.path = path; this.type = type; this.size = size; + this.ino = ino; + this.inode = inode; + this.lastModified = lastModified; + this.lastAccessed = lastAccessed; + this.creationTime = creationTime; + this.drive = drive; + this.dev = dev; + this.nlink = nlink; + this.uid = uid; + this.gid = gid; + this.mode = mode; + this.isSymbolicLink = isSymbolicLink; this.dataPath = new DataPath(drive, path); this.protectedFile = protectedFile; } @@ -52,7 +73,18 @@ public static FileInfo fromTag(CompoundTag compoundTag) { Path.of(compoundTag.getString("path")), compoundTag.getBoolean("folder") ? FSEntryType.FOLDER : FSEntryType.FILE, compoundTag.getLong("size"), - compoundTag.getBoolean("protected") + compoundTag.getLong("ino"), + compoundTag.getLong("lastModified"), + compoundTag.getLong("lastAccessed"), + compoundTag.getLong("creationTime"), + compoundTag.getLong("inode"), + compoundTag.getLong("dev"), + compoundTag.getByte("nlink"), + compoundTag.getInt("uid"), + compoundTag.getInt("gid"), + compoundTag.getBoolean("protected"), + compoundTag.getShort("mode"), + compoundTag.getBoolean("isSymbolicLink") ); } @@ -225,7 +257,7 @@ public void child(String name, Consumer> callback) { } public FileInfo withExtension(String name) { - FileInfo fileInfo = new FileInfo(drive, path.toString().endsWith(name) ? path : path.resolveSibling(getName() + "." + name), type, size, protectedFile); + FileInfo fileInfo = new FileInfo(drive, path.toString().endsWith(name) ? path : path.resolveSibling(getName() + "." + name), type, size, ino, lastModified, lastAccessed, creationTime, inode, dev, nlink, uid, gid, protectedFile, mode, isSymbolicLink); fileInfo.size = size; fileInfo.lastAccessed = lastAccessed; fileInfo.lastModified = lastModified; @@ -236,4 +268,52 @@ public FileInfo withExtension(String name) { public long getSize() { return size; } + + public long getIno() { + return ino; + } + + public long getCreationTime() { + return creationTime; + } + + public long getLastAccessed() { + return lastAccessed; + } + + public long getLastModified() { + return lastModified; + } + + public long getDev() { + return dev; + } + + public long getInode() { + return inode; + } + + public int getFileKey() { + return fileKey; + } + + public int getNlink() { + return nlink; + } + + public boolean isSymbolicLink() { + return isSymbolicLink; + } + + public int getGid() { + return gid; + } + + public int getUid() { + return uid; + } + + public int getMode() { + return mode; + } } diff --git a/common/src/main/java/com/ultreon/devices/programs/terminal/TerminalApp.java b/common/src/main/java/com/ultreon/devices/programs/terminal/TerminalApp.java new file mode 100644 index 000000000..f88b0fa75 --- /dev/null +++ b/common/src/main/java/com/ultreon/devices/programs/terminal/TerminalApp.java @@ -0,0 +1,33 @@ +package com.ultreon.devices.programs.terminal; + +import com.ultreon.devices.api.app.Application; +import com.ultreon.devices.core.Laptop; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.nbt.CompoundTag; +import org.jetbrains.annotations.Nullable; + +public class TerminalApp extends Application { + private final TerminalLayout terminalLayout = new TerminalLayout(); + + @Override + public void init(@Nullable CompoundTag intent) { + terminalLayout.setTitle("Terminal"); + setCurrentLayout(terminalLayout); + + terminalLayout.init(); + + this.setDefaultWidth(terminalLayout.width); + this.setDefaultHeight(terminalLayout.height); + } + + @Override + public void load(CompoundTag tag) { + + } + + @Override + public void save(CompoundTag tag) { + + } +} diff --git a/common/src/main/java/com/ultreon/devices/programs/terminal/TerminalLayout.java b/common/src/main/java/com/ultreon/devices/programs/terminal/TerminalLayout.java new file mode 100644 index 000000000..17f91b1a6 --- /dev/null +++ b/common/src/main/java/com/ultreon/devices/programs/terminal/TerminalLayout.java @@ -0,0 +1,308 @@ +package com.ultreon.devices.programs.terminal; + +import com.ultreon.devices.api.app.Layout; +import com.ultreon.devices.core.Laptop; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import org.lwjgl.glfw.GLFW; + +import java.util.Arrays; +import java.util.function.Consumer; + +public class TerminalLayout extends Layout { + private String[] keyCodes = null; + private char[][] matrix; + private int cursorX, cursorY; + private int lock = -1; + private Consumer inputHandler; + private Consumer oldInputHandler; + + public TerminalLayout() { + super(); + } + + @Override + public void init() { + super.init(); + + cursorX = 0; + cursorY = 0; + lock = -1; + matrix = new char[20][50]; + this.height = matrix.length * 8 + 16; + this.width = matrix[0].length * 8 + 16; + } + + private void inputHandler(String code) { + if (code.startsWith("PRESSED:")) { + System.out.println(code); + + switch (code) { + case "PRESSED:ENTER" -> { + newLine(); + inputHandler = oldInputHandler; + } + case "PRESSED:BACKSPACE" -> backspace(); + case "PRESSED:LEFT" -> { + cursorX--; + if (cursorX < 0 || cursorX < lock) { + cursorX++; + } + } + case "PRESSED:RIGHT" -> { + if (getchr(cursorX, cursorY) != 0) { + cursorX++; + } + } + } + } else if (code.startsWith("TYPED:")) { + char c = code.charAt(6); + print(c, cursorX, cursorY); + } + } + + @Override + public void render(GuiGraphics graphics, Laptop laptop, Minecraft mc, int x, int y, int mouseX, int mouseY, boolean windowActive, float partialTicks) { + super.render(graphics, laptop, mc, x, y, mouseX, mouseY, windowActive, partialTicks); + + if (keyCodes == null) { + keyCodes = new String[GLFW.GLFW_KEY_LAST]; + for (int i = 0; i < GLFW.GLFW_KEY_LAST; i++) { + keyCodes[i] = GLFW.glfwGetKeyName(i, 0); + } + } + + graphics.fill(0, 0, x + width, y + height, 0xff000000); + graphics.fill(x, y, x + width, y + height, 0xff000000); + + for (int i = 0; i < matrix.length; i++) { + for (int j = 0; j < matrix[i].length; j++) { + char c = matrix[i][j]; + if (c != 0) { + graphics.drawString(mc.font, String.valueOf(c), x + j * 8 + 8 - Minecraft.getInstance().font.width(String.valueOf(c)) / 2, y + i * 8 + 4, 0xFFFFFF); + } + + if (j == cursorX && i == cursorY) { + graphics.drawString(mc.font, "_", x + j * 8 + 8 - Minecraft.getInstance().font.width(String.valueOf("_")) / 2, y + i * 8 + 4, 0xFFFFFF); + } + } + } + } + + public void print(char c, int x, int y) { + cursorX = x; + cursorY = y; + + for (int i = matrix[0].length - 2; i >= x; i--) { + matrix[y][i + 1] = matrix[y][i]; + } + + matrix[y][x] = c; + + forward(); + } + + private void forward() { + cursorX++; + if (cursorX == matrix[0].length) { + cursorX = 0; + cursorY++; + + if (cursorY == matrix.length) { + cursorY--; + scroll(); + } + } + } + + public void print(String s, int x, int y) { + cursorX = x; + cursorY = y; + for (int i = 0; i < s.length(); i++) { + matrix[y][x] = s.charAt(i); + x++; + if (x == matrix[0].length) { + x = 0; + y++; + + if (y == matrix.length) { + y--; + scroll(); + } + } + + cursorX = x; + cursorY = y; + } + } + + public void putchr(char c, int x, int y) { + matrix[y][x] = c; + } + + public char getchr(int x, int y) { + if (x < 0 || x >= matrix[0].length || y < 0 || y >= matrix.length) { + return 0; + } + return matrix[y][x]; + } + + public void putstr(String s, int x, int y) { + for (int i = 0; i < s.length(); i++) { + matrix[y][x] = s.charAt(i); + x++; + + if (x == matrix[0].length) { + x = 0; + y++; + + if (y == matrix.length) { + y--; + scroll(); + } + } + } + } + + public String getstr(int x, int y, int length) { + StringBuilder s = new StringBuilder(); + for (int i = 0; i < length; i++) { + s.append(matrix[y][x]); + x++; + + if (x == matrix[0].length) { + x = 0; + y++; + + if (y == matrix.length) { + y--; + scroll(); + } + } + } + + return s.toString(); + } + + public void backspace() { + if (cursorX > 0 && cursorX >= lock) { + for (int i = cursorX - 1; i < matrix[0].length - 1; i++) { + putchr(getchr(i + 1, cursorY), i, cursorY); + } + cursorX--; + } + } + + public void setLocation(int x, int y) { + cursorX = x; + cursorY = y; + } + + public void setLock(int lock) { + this.lock = lock; + } + + public int getLock() { + return lock; + } + + public void resetLock() { + this.lock = -1; + } + + public void interceptInput(Consumer handler) { + this.inputHandler = handler; + } + + public void scroll() { + for (int i = 0; i < matrix.length - 1; i++) { + matrix[i] = matrix[i + 1]; + } + + Arrays.fill(matrix[matrix.length - 1], (char) 0); + } + + public void clear() { + for (char[] chars : matrix) { + Arrays.fill(chars, (char) 0); + } + } + + public void clear(int x, int y) { + matrix[y][x] = 0; + } + + public void input(String s) { + print(s, cursorX, cursorY); + lock = cursorX; + oldInputHandler = inputHandler; + inputHandler = this::inputHandler; + } + + @Override + public void handleKeyPressed(int keyCode, int scanCode, int modifiers) { + if (inputHandler != null) { + inputHandler.accept("PRESSED:" + getKey(keyCode, modifiers)); + } else { + if (keyCode == GLFW.GLFW_KEY_BACKSPACE) { + backspace(); + } + + if (keyCode == GLFW.GLFW_KEY_ENTER) { + newLine(); + } + + if (keyCode == GLFW.GLFW_KEY_LEFT) { + if (cursorX > 0 && cursorX > lock) { + cursorX--; + } + } + + if (keyCode == GLFW.GLFW_KEY_RIGHT) { + if (getchr(cursorX, cursorY) != 0) { + cursorX++; + } + } + } + } + + private void newLine() { + cursorY++; + cursorX = 0; + if (cursorY == matrix.length) { + cursorY--; + scroll(); + } + } + + public void handleKeyReleased(int keyCode, int scanCode, int modifiers) { + if (inputHandler != null) { + inputHandler.accept("RELEASED:" + getKey(keyCode, modifiers)); + } + } + + @Override + public void handleCharTyped(char codePoint, int modifiers) { + if (inputHandler != null) { + inputHandler.accept("TYPED:" + codePoint); + } else { + print(codePoint, cursorX, cursorY); + } + } + + private String getKey(int keyCode, int modifiers) { + int glfwModShift = GLFW.GLFW_MOD_SHIFT; + int glfwModControl = GLFW.GLFW_MOD_CONTROL; + int glfwModAlt = GLFW.GLFW_MOD_ALT; + + if ((modifiers & glfwModShift) == glfwModShift) { + return "SHIFT:" + keyCodes[keyCode]; + } else if ((modifiers & glfwModControl) == glfwModControl) { + return "CTRL:" + keyCodes[keyCode]; + } else if ((modifiers & glfwModAlt) == glfwModAlt) { + return "ALT:" + keyCodes[keyCode]; + } else { + return ":" + keyCodes[keyCode]; + } + } +} diff --git a/common/src/main/resources/assets/devices/apps/terminal.json b/common/src/main/resources/assets/devices/apps/terminal.json new file mode 100644 index 000000000..16d472c37 --- /dev/null +++ b/common/src/main/resources/assets/devices/apps/terminal.json @@ -0,0 +1,10 @@ +{ + "schemaVersion": 2, + "name": "Terminal", + "authors": ["Qubilux"], + "description": "Essential terminal app", + "version": "1.0", + "screenshots": [ + "https://i.imgur.com/7CzbiA7.png" + ] +} \ No newline at end of file diff --git a/common/src/main/resources/assets/devices/models/block/mac_max_x.json b/common/src/main/resources/assets/devices/models/block/mac_max_x.json index 8f3de0c4d..5bd71cad5 100644 --- a/common/src/main/resources/assets/devices/models/block/mac_max_x.json +++ b/common/src/main/resources/assets/devices/models/block/mac_max_x.json @@ -1,7 +1,7 @@ { "credit": "Made with Blockbench", - "texture_size": [128, 128], "ambientocclusion": false, + "texture_size": [128, 128], "textures": { "0": "devices:block/mac_max_x" }, @@ -159,13 +159,13 @@ "scale": [0.3, 0.3, 0.3] }, "ground": { - "translation": [0, 3.5, 0], - "scale": [0.45, 0.45, 0.45] + "translation": [0, 3, 0], + "scale": [0.25, 0.25, 0.25] }, "gui": { - "rotation": [0, 180, 0], - "translation": [0, -2.5, 4.75], - "scale": [0.35, 0.35, 0.35] + "rotation": [30, 225, 0], + "translation": [0, -2.5, 0], + "scale": [0.375, 0.375, 0.375] }, "head": { "rotation": [85, 0, 0], @@ -173,8 +173,7 @@ "scale": [0.8, 0.8, 0.8] }, "fixed": { - "translation": [0, -6.25, -0.5], - "scale": [0.66, 0.66, 0.66] + "scale": [0.5, 0.5, 0.5] } }, "groups": [ @@ -182,8 +181,6 @@ "name": "VoxelShapes", "origin": [8, 8, 8], "color": 0, - "nbt": "{}", - "armAnimationEnabled": false, "children": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] } ] diff --git a/fabric/build.gradle b/fabric/build.gradle index dd8a8d8c2..2be0b196d 100644 --- a/fabric/build.gradle +++ b/fabric/build.gradle @@ -48,6 +48,8 @@ dependencies { modImplementation 'com.electronwill.night-config:core:3.6.5' modImplementation 'com.electronwill.night-config:toml:3.6.5' + include implementation("org.graalvm.polyglot:polyglot:24.1.1") + include implementation("org.graalvm.python:python-community:24.1.1") // include(implementation("org.jetbrains.kotlin:kotlin-reflect:1.7.10")) // include(implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10")) // include(implementation("com.ultreon:ultranlang:0.1.0+6")) diff --git a/neoforge/build.gradle b/neoforge/build.gradle index 86d329cee..ea627947f 100644 --- a/neoforge/build.gradle +++ b/neoforge/build.gradle @@ -38,6 +38,8 @@ dependencies { common(project(path: ":common", configuration: "namedElements")) { transitive false } shadowCommon(project(path: ":common", configuration: "transformProductionNeoForge")) { transitive = false } + include implementation("org.graalvm.polyglot:polyglot:24.1.1") + include implementation("org.graalvm.python:python-community:24.1.1") // forgeRuntimeLibrary "com.ultreon:ultranlang:0.1.0+6" // implementation include("com.ultreon:ultranlang:0.1.0+6") diff --git a/neoforge/src/main/java/com/ultreon/devices/neoforge/ForgeApplicationRegistration.java b/neoforge/src/main/java/com/ultreon/devices/neoforge/ForgeApplicationRegistration.java index 1e263b3fb..ad824834f 100644 --- a/neoforge/src/main/java/com/ultreon/devices/neoforge/ForgeApplicationRegistration.java +++ b/neoforge/src/main/java/com/ultreon/devices/neoforge/ForgeApplicationRegistration.java @@ -1,7 +1,7 @@ package com.ultreon.devices.neoforge; -import net.minecraftforge.eventbus.ap.Event; -import net.minecraftforge.fml.event.IModBusEvent; +//import net.minecraftforge.eventbus.ap.Event; +//import net.minecraftforge.fml.event.IModBusEvent; public class ForgeApplicationRegistration extends Event implements IModBusEvent { } diff --git a/quilt/src/main/java/com/ultreon/devices/quilt/DevicesQuiltMod.java b/quilt/src/main/java/com/ultreon/devices/quilt/DevicesQuiltMod.java index d30169db7..56863aa33 100644 --- a/quilt/src/main/java/com/ultreon/devices/quilt/DevicesQuiltMod.java +++ b/quilt/src/main/java/com/ultreon/devices/quilt/DevicesQuiltMod.java @@ -7,14 +7,14 @@ import com.ultreon.devices.api.print.PrintingManager; import com.ultreon.devices.core.Laptop; import com.ultreon.devices.init.RegistrationHandler; -import fuzs.forgeconfigapiport.api.config.v2.ForgeConfigRegistry; +//import fuzs.forgeconfigapiport.api.config.v2.ForgeConfigRegistry; import net.fabricmc.api.ModInitializer; import net.fabricmc.loader.api.FabricLoader; import net.fabricmc.loader.api.entrypoint.EntrypointContainer; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.crafting.RecipeType; import net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity; -import net.minecraftforge.fml.config.ModConfig; +//import net.minecraftforge.fml.config.ModConfig; import java.util.ArrayList; import java.util.List; @@ -23,7 +23,7 @@ public class DevicesQuiltMod extends Devices implements ModInitializer { @Override public void onInitialize() { - ForgeConfigRegistry.INSTANCE.register(Devices.MOD_ID, ModConfig.Type.CLIENT, DeviceConfig.CONFIG); +// ForgeConfigRegistry.INSTANCE.register(Devices.MOD_ID, ModConfig.Type.CLIENT, DeviceConfig.CONFIG); this.init(); diff --git a/settings.gradle b/settings.gradle index 12417d695..3339de374 100644 --- a/settings.gradle +++ b/settings.gradle @@ -14,4 +14,6 @@ include("fabric") include("neoforge") include("quilt") include("fabric-datagen-helper") -include("fabric-testmod") \ No newline at end of file +include("fabric-testmod") +include 'bootimage' +