diff --git a/assets/Scripts/compiler/compiler.js b/assets/Scripts/compiler/compiler.js index fc9cadbb..6ea525fa 100644 --- a/assets/Scripts/compiler/compiler.js +++ b/assets/Scripts/compiler/compiler.js @@ -60,6 +60,28 @@ let COMPILER = { */ currentDirectory : { }, + + /** + * Preloads a file from the host system's cache + * @param {*} fileName The name of the file + * @param {*} content The content of the file + */ + preloadFile: (fileName, content) => { + COMPILER.fileMap[fileName] = COMPILER.createFile(fileName, content) + COMPILER.fileMap[fileName].moduleContent = COMPILER.getModuleContent(content) + }, + + /** + * Gets the module content from generic file content + * @param {*} content The file content + * @returns The module content + */ + getModuleContent: (content) => { + return "let exports = { }\n" + + content + "\n" + + "return exports" + }, + /** * Registers a file with the compiler * @param {string} fileName The file's name @@ -409,10 +431,7 @@ COMPILER.customCompilerHost = { writeFile: (fileName, data) => { loggerScripts.INFO("EMIT FILE " + fileName) //wrap in require logic - let finalData = - "let exports = { }\n" + - data + "\n" + - "return exports" + let finalData = COMPILER.getModuleContent(data) //create file COMPILER.createFile(fileName,finalData) diff --git a/docs/src/progress/renderertodo.md b/docs/src/progress/renderertodo.md index 9980197f..689f61cc 100644 --- a/docs/src/progress/renderertodo.md +++ b/docs/src/progress/renderertodo.md @@ -1042,6 +1042,13 @@ Add concept of interaction definition in common entity type Fix bug with entity ray casting selecting player entity Fix crafting menu yoga appliation logic Workbench can open crafting menu +Fix character customization panel layout + +(11/15/2024) +Script engine preloading +Fix YogaUtils.refreshComponent breaking when passed null window +Remove FontUtils .testcache creation +File watching scripts source dir @@ -1073,7 +1080,6 @@ Implement gadgets - Recipe definitions - Reagent items - Ability to fully reload game engine state without exiting client - Back out to main menu and load a new level without any values persisting - Receive a teleport packet from server and flush all game state before requesting state from server again @@ -1083,7 +1089,6 @@ Bug Fixes - Fix not all grass tiles update when updating a nearby voxel (ie it doesn't go into negative coordinates to scan for foliage updates) - Fix typescript load error - Calculate bounding sphere for meshes by deforming vertices with bone default pose instead of no bone deform - - Fix character creation menu - Fix threads not synchronizing when returning to main menu (rendering still running when player entity deleted, race condition) - Fix light cluster mapping for foliage shader - Fix foliage placement @@ -1092,8 +1097,8 @@ Bug Fixes - Fix block tree preventing initiating an attack - Fix return to title menu synchronization bug - Fix particles not spawning in correct positions - - Fix level creation menu button alignment - - Fix spawn palette menu alignment + - Fix flickering when applying yoga signal (may need to rethink arch here) + - Fix virtual scrollables not working Startup Performance - Cache loaded typescript @@ -1127,16 +1132,6 @@ Code cleanup - Rename "ShaderProgram" to "VisualShader" - Have ComputeShader and VisualShader use same static method for uploading uniforms -Transvoxel implementation - - Fix draw cell manager requesting far-out chunks - - Properly update to higher LOD meshes as you get closer - Client Terrain Entity Management (specifically creation/teardown for client) - - Also queries for far out chunks to load far away terrain - Server Terrain Management (specifically for collision) - - Handles communicating far out LOD chunks to client as well - Terrain Interface Positional Access Interface - - Ability to get terrain at point for interactions with game world eg placing grass/water collision - Build system to allow specifying certain audio files to load as stereo Rework how chunks are written to disk to make them more cache friendly @@ -1186,8 +1181,6 @@ Revisit first attempt at instancing (its really laggy lol) Shader library system - Abiltiy to include the shader library in individual files (ie implement #include) -Break control handlers into separate files with new logic to transition between control handler states - Another pass at grass - Multiple foliage models in same cell diff --git a/src/main/java/electrosphere/client/item/ItemActions.java b/src/main/java/electrosphere/client/item/ItemActions.java index 0b357586..0e59011c 100644 --- a/src/main/java/electrosphere/client/item/ItemActions.java +++ b/src/main/java/electrosphere/client/item/ItemActions.java @@ -40,6 +40,9 @@ public class ItemActions { Vector3d eyePos = new Vector3d(CameraEntityUtils.getCameraEye(Globals.playerCamera)); Vector3d centerPos = new Vector3d(CameraEntityUtils.getCameraCenter(Globals.playerCamera)); Vector3d cursorPos = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(centerPos), new Vector3d(eyePos).mul(-1.0), CollisionEngine.DEFAULT_INTERACT_DISTANCE); + if(cursorPos == null){ + cursorPos = new Vector3d(centerPos).add(new Vector3d(eyePos).normalize().mul(-CollisionEngine.DEFAULT_INTERACT_DISTANCE)); + } //tell the server we want the secondary hand item to START doing something Globals.clientConnection.queueOutgoingMessage(InventoryMessage.constructclientRequestPerformItemActionMessage( "handRight", @@ -77,6 +80,9 @@ public class ItemActions { Vector3d eyePos = new Vector3d(CameraEntityUtils.getCameraEye(Globals.playerCamera)); Vector3d centerPos = new Vector3d(CameraEntityUtils.getCameraCenter(Globals.playerCamera)); Vector3d cursorPos = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(centerPos), new Vector3d(eyePos).mul(-1.0), CollisionEngine.DEFAULT_INTERACT_DISTANCE); + if(cursorPos == null){ + cursorPos = new Vector3d(centerPos).add(new Vector3d(eyePos).normalize().mul(-CollisionEngine.DEFAULT_INTERACT_DISTANCE)); + } //tell the server we want the secondary hand item to STOP doing something Globals.clientConnection.queueOutgoingMessage(InventoryMessage.constructclientRequestPerformItemActionMessage( "handRight", @@ -96,6 +102,9 @@ public class ItemActions { Vector3d eyePos = new Vector3d(CameraEntityUtils.getCameraEye(Globals.playerCamera)); Vector3d centerPos = new Vector3d(CameraEntityUtils.getCameraCenter(Globals.playerCamera)); Vector3d cursorPos = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(centerPos), new Vector3d(eyePos).mul(-1.0), CollisionEngine.DEFAULT_INTERACT_DISTANCE); + if(cursorPos == null){ + cursorPos = new Vector3d(centerPos).add(new Vector3d(eyePos).normalize().mul(-CollisionEngine.DEFAULT_INTERACT_DISTANCE)); + } //tell the server we want the secondary hand item to STOP doing something Globals.clientConnection.queueOutgoingMessage(InventoryMessage.constructclientRequestPerformItemActionMessage( "handRight", @@ -123,6 +132,9 @@ public class ItemActions { Vector3d eyePos = new Vector3d(CameraEntityUtils.getCameraEye(Globals.playerCamera)); Vector3d centerPos = new Vector3d(CameraEntityUtils.getCameraCenter(Globals.playerCamera)); Vector3d cursorPos = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(centerPos), new Vector3d(eyePos).mul(-1.0), CollisionEngine.DEFAULT_INTERACT_DISTANCE); + if(cursorPos == null){ + cursorPos = new Vector3d(centerPos).add(new Vector3d(eyePos).normalize().mul(-CollisionEngine.DEFAULT_INTERACT_DISTANCE)); + } //tell the server we want the secondary hand item to START doing something Globals.clientConnection.queueOutgoingMessage(InventoryMessage.constructclientRequestPerformItemActionMessage( "handRight", @@ -152,6 +164,9 @@ public class ItemActions { Vector3d eyePos = new Vector3d(CameraEntityUtils.getCameraEye(Globals.playerCamera)); Vector3d centerPos = new Vector3d(CameraEntityUtils.getCameraCenter(Globals.playerCamera)); Vector3d cursorPos = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(centerPos), new Vector3d(eyePos).mul(-1.0), CollisionEngine.DEFAULT_INTERACT_DISTANCE); + if(cursorPos == null){ + cursorPos = new Vector3d(centerPos).add(new Vector3d(eyePos).normalize().mul(-CollisionEngine.DEFAULT_INTERACT_DISTANCE)); + } //tell the server we want the secondary hand item to STOP doing something Globals.clientConnection.queueOutgoingMessage(InventoryMessage.constructclientRequestPerformItemActionMessage( "handRight", diff --git a/src/main/java/electrosphere/client/script/ClientScriptUtils.java b/src/main/java/electrosphere/client/script/ClientScriptUtils.java index 2a90e880..c1eb9ba6 100644 --- a/src/main/java/electrosphere/client/script/ClientScriptUtils.java +++ b/src/main/java/electrosphere/client/script/ClientScriptUtils.java @@ -14,9 +14,9 @@ public class ClientScriptUtils { * @param args The arguments provided alongside the signal */ public static void fireSignal(String signalName, Object ... args){ - Globals.scriptEngine.executeSynchronously(() -> { + Globals.scriptEngine.getScriptContext().executeSynchronously(() -> { if(Globals.scriptEngine != null && Globals.scriptEngine.isInitialized()){ - Globals.scriptEngine.fireSignal(signalName, ScriptEngine.GLOBAL_SCENE, args); + Globals.scriptEngine.getScriptContext().fireSignal(signalName, ScriptEngine.GLOBAL_SCENE, args); } }); } diff --git a/src/main/java/electrosphere/client/ui/components/CharacterCustomizer.java b/src/main/java/electrosphere/client/ui/components/CharacterCustomizer.java index 6a1d41f1..31db0641 100644 --- a/src/main/java/electrosphere/client/ui/components/CharacterCustomizer.java +++ b/src/main/java/electrosphere/client/ui/components/CharacterCustomizer.java @@ -1,5 +1,6 @@ package electrosphere.client.ui.components; +import java.util.function.Consumer; import java.util.stream.Collectors; import org.joml.Vector3f; @@ -45,7 +46,7 @@ public class CharacterCustomizer { * @param race The race of the character * @return The panel component */ - public static Element createCharacterCustomizerPanel(String race){ + public static Element createCharacterCustomizerPanel(String race, Consumer onConfirm){ //figure out race data CreatureData selectedRaceType = Globals.gameConfigCurrent.getCreatureTypeLoader().getType(race); @@ -149,6 +150,7 @@ public class CharacterCustomizer { ), Button.createButton("Create", () -> { Globals.clientConnection.queueOutgoingMessage(CharacterMessage.constructRequestCreateCharacterMessage(Utilities.stringify(template))); + onConfirm.accept(template); }) ); diff --git a/src/main/java/electrosphere/client/ui/components/VoxelSelectionPanel.java b/src/main/java/electrosphere/client/ui/components/VoxelSelectionPanel.java new file mode 100644 index 00000000..a0912fa3 --- /dev/null +++ b/src/main/java/electrosphere/client/ui/components/VoxelSelectionPanel.java @@ -0,0 +1,176 @@ +package electrosphere.client.ui.components; + +import java.util.List; +import java.util.function.Consumer; + +import org.joml.Vector3f; + +import electrosphere.client.ui.menu.WindowStrings; +import electrosphere.client.ui.menu.YogaUtils; +import electrosphere.engine.Globals; +import electrosphere.game.data.voxel.VoxelData; +import electrosphere.game.data.voxel.VoxelType; +import electrosphere.renderer.ui.elements.Button; +import electrosphere.renderer.ui.elements.Div; +import electrosphere.renderer.ui.elements.ImagePanel; +import electrosphere.renderer.ui.elements.Label; +import electrosphere.renderer.ui.elements.TextInput; +import electrosphere.renderer.ui.elements.VirtualScrollable; +import electrosphere.renderer.ui.elementtypes.ClickableElement.ClickEventCallback; +import electrosphere.renderer.ui.elementtypes.ContainerElement.YogaAlignment; +import electrosphere.renderer.ui.elementtypes.ContainerElement.YogaFlexDirection; +import electrosphere.renderer.ui.elementtypes.ContainerElement.YogaJustification; +import electrosphere.renderer.ui.elementtypes.Element; +import electrosphere.renderer.ui.elementtypes.HoverableElement.HoverEventCallback; +import electrosphere.renderer.ui.elementtypes.KeyEventElement.KeyboardEventCallback; +import electrosphere.renderer.ui.events.ClickEvent; +import electrosphere.renderer.ui.events.HoverEvent; +import electrosphere.renderer.ui.events.KeyboardEvent; + +/** + * A panel that provides a voxel selection + */ +public class VoxelSelectionPanel { + + //text input + static final int TEXT_INPUT_HEIGHT = 50; + static final int TEXT_INPUT_WIDTH = 200; + + //single voxel button + static final int VOXEL_BUTTON_WIDTH = 90; + static final int VOXEL_BUTTON_HEIGHT = 90; + static final int VOXEL_BUTTON_TEXTURE_DIM = 70; + static final int MARGIN_EACH_SIDE = 5; + + //voxel selection + static final int VOXEL_SCROLLABLE_WIDTH = VOXEL_BUTTON_WIDTH * 5; + static final int VOXEL_SCROLLABLE_HEIGHT = VOXEL_BUTTON_HEIGHT * 5; + + /** + * The color of the select voxel type + */ + static final Vector3f ELEMENT_COLOR_SELECTED = new Vector3f(1,0,0); + + + /** + * Creates the level editor side panel top view + * @return + */ + public static Div createVoxelTypeSelectionPanel(Consumer onSelectType){ + //setup window + Div rVal = Div.createDiv(); + rVal.setAlignContent(YogaAlignment.Center); + rVal.setAlignItems(YogaAlignment.Center); + rVal.setJustifyContent(YogaJustification.Center); + rVal.setFlexDirection(YogaFlexDirection.Column); + + //scrollable that contains all the voxel types + VirtualScrollable scrollable = new VirtualScrollable(VOXEL_SCROLLABLE_WIDTH, VOXEL_SCROLLABLE_HEIGHT); + scrollable.setFlexDirection(YogaFlexDirection.Column); + scrollable.setAlignItems(YogaAlignment.Start); + + //search input + TextInput searchInput = TextInput.createTextInput(); + searchInput.setWidth(TEXT_INPUT_WIDTH); + searchInput.setMinWidth(TEXT_INPUT_WIDTH); + searchInput.setMinHeight(20); + searchInput.setOnPress(new KeyboardEventCallback() {public boolean execute(KeyboardEvent event){ + boolean rVal = searchInput.defaultKeyHandling(event); + VoxelSelectionPanel.fillInVoxelSelectors(scrollable, searchInput.getText(), onSelectType); + return rVal; + }}); + rVal.addChild(searchInput); + + + //attach scrollable after search input for organzation purposes + rVal.addChild(scrollable); + + //final step + VoxelSelectionPanel.fillInVoxelSelectors(scrollable, searchInput.getText(), onSelectType); + + return rVal; + } + + /** + * Fills in the voxels to display based on the contents of the search string + * @param scrollable the scrollable to drop selection buttons in to + * @param searchString the string to search based on + */ + static void fillInVoxelSelectors(VirtualScrollable scrollable, String searchString, Consumer onSelectType){ + Element containingWindow = null; + if(Globals.elementService.getWindow(WindowStrings.VOXEL_TYPE_SELECTION) != null){ + containingWindow = Globals.elementService.getWindow(WindowStrings.VOXEL_TYPE_SELECTION); + } else if(Globals.elementService.getWindow(WindowStrings.WINDOW_MENU_MAIN) != null){ + containingWindow = Globals.elementService.getWindow(WindowStrings.WINDOW_MENU_MAIN); + } + YogaUtils.refreshComponent(containingWindow, () -> { + scrollable.clearChildren(); + VoxelData voxelData = Globals.gameConfigCurrent.getVoxelData(); + List matchingVoxels = voxelData.getTypes().stream().filter((type)->type.getName().toLowerCase().contains(searchString.toLowerCase())).toList(); + Div currentRow = null; + int incrementer = 0; + //generate voxel buttons + for(VoxelType type : matchingVoxels){ + if(incrementer % 4 == 0){ + currentRow = Div.createRow(); + currentRow.setJustifyContent(YogaJustification.Evenly); + scrollable.addChild(currentRow); + } + Div containerDiv = Div.createDiv(); + containerDiv.setMinWidthPercent(25.0f); + currentRow.addChild(containerDiv); + + Button newButton = new Button(); + newButton.setAlignItems(YogaAlignment.Center); + //dimensions + newButton.setMinWidth(VOXEL_BUTTON_WIDTH); + newButton.setMinHeight(VOXEL_BUTTON_HEIGHT); + //margin + newButton.setMarginBottom(MARGIN_EACH_SIDE); + newButton.setMarginLeft(MARGIN_EACH_SIDE); + newButton.setMarginRight(MARGIN_EACH_SIDE); + newButton.setMarginTop(MARGIN_EACH_SIDE); + //set color if this is the selected voxel type + if(type == Globals.clientSelectedVoxelType){ + newButton.setColor(ELEMENT_COLOR_SELECTED); + } + //label + Label voxelLabel = Label.createLabel(type.getName()); + //icon/model + ImagePanel texturePanel = ImagePanel.createImagePanel(type.getTexture()); + if(type.getTexture() != null){ + Globals.assetManager.addTexturePathtoQueue(type.getTexture()); + } + texturePanel.setWidth(VOXEL_BUTTON_TEXTURE_DIM); + texturePanel.setHeight(VOXEL_BUTTON_TEXTURE_DIM); + texturePanel.setMarginBottom(MARGIN_EACH_SIDE); + texturePanel.setMarginLeft(MARGIN_EACH_SIDE); + texturePanel.setMarginRight(MARGIN_EACH_SIDE); + texturePanel.setMarginTop(MARGIN_EACH_SIDE); + newButton.addChild(texturePanel); + texturePanel.setAlignSelf(YogaAlignment.Center); + //causes the texture panel to also behave as if the button was hovered + texturePanel.setOnHoverCallback(new HoverEventCallback() {public boolean execute(HoverEvent event) { + return newButton.handleEvent(event); + }}); + //button handling + newButton.addChild(voxelLabel); + newButton.setOnClick(new ClickEventCallback() {public boolean execute(ClickEvent event){ + //set voxel type to this type + onSelectType.accept(type); + Globals.clientSelectedVoxelType = type; + VoxelSelectionPanel.fillInVoxelSelectors(scrollable, searchString, onSelectType); + return false; + }}); + containerDiv.addChild(newButton); + incrementer++; + } + for(int i = incrementer; i % 4 != 0; i++){ + Div spacerDiv = Div.createDiv(); + spacerDiv.setMinWidthPercent(25.0f); + currentRow.addChild(spacerDiv); + } + }); + } + +} diff --git a/src/main/java/electrosphere/client/ui/menu/YogaUtils.java b/src/main/java/electrosphere/client/ui/menu/YogaUtils.java new file mode 100644 index 00000000..0e5e8fab --- /dev/null +++ b/src/main/java/electrosphere/client/ui/menu/YogaUtils.java @@ -0,0 +1,24 @@ +package electrosphere.client.ui.menu; + +import electrosphere.engine.Globals; +import electrosphere.engine.signal.Signal.SignalType; +import electrosphere.renderer.ui.elementtypes.Element; + +/** + * Utilities for working with yoga + */ +public class YogaUtils { + + /** + * Refreshes a component + * @param containingWindow The window containing the component + * @param render The function that renders the component's content + */ + public static void refreshComponent(Element containingWindow, Runnable render){ + Globals.signalSystem.post(SignalType.UI_MODIFICATION,render); + if(containingWindow != null){ + Globals.signalSystem.post(SignalType.YOGA_APPLY, containingWindow); + } + } + +} diff --git a/src/main/java/electrosphere/client/ui/menu/ingame/MenuGeneratorsTerrainEditing.java b/src/main/java/electrosphere/client/ui/menu/ingame/MenuGeneratorsTerrainEditing.java index 31bfc7d5..0b0d74e4 100644 --- a/src/main/java/electrosphere/client/ui/menu/ingame/MenuGeneratorsTerrainEditing.java +++ b/src/main/java/electrosphere/client/ui/menu/ingame/MenuGeneratorsTerrainEditing.java @@ -1,32 +1,19 @@ package electrosphere.client.ui.menu.ingame; -import java.util.List; -import java.util.function.Consumer; - import electrosphere.client.ui.components.SpawnSelectionPanel; +import electrosphere.client.ui.components.VoxelSelectionPanel; import electrosphere.client.ui.menu.WindowStrings; import electrosphere.client.ui.menu.WindowUtils; import electrosphere.controls.ControlHandler.ControlsState; import electrosphere.engine.Globals; import electrosphere.engine.signal.Signal.SignalType; import electrosphere.game.data.common.CommonEntityType; -import electrosphere.game.data.voxel.VoxelData; import electrosphere.game.data.voxel.VoxelType; -import electrosphere.renderer.ui.elements.Button; -import electrosphere.renderer.ui.elements.Div; -import electrosphere.renderer.ui.elements.ImagePanel; -import electrosphere.renderer.ui.elements.Label; -import electrosphere.renderer.ui.elements.TextInput; -import electrosphere.renderer.ui.elements.VirtualScrollable; import electrosphere.renderer.ui.elements.Window; -import electrosphere.renderer.ui.elementtypes.ClickableElement.ClickEventCallback; import electrosphere.renderer.ui.elementtypes.ContainerElement.YogaAlignment; import electrosphere.renderer.ui.elementtypes.ContainerElement.YogaFlexDirection; import electrosphere.renderer.ui.elementtypes.ContainerElement.YogaJustification; -import electrosphere.renderer.ui.elementtypes.KeyEventElement.KeyboardEventCallback; import electrosphere.renderer.ui.elementtypes.NavigableElement.NavigationEventCallback; -import electrosphere.renderer.ui.events.ClickEvent; -import electrosphere.renderer.ui.events.KeyboardEvent; import electrosphere.renderer.ui.events.NavigationEvent; /** @@ -34,23 +21,8 @@ import electrosphere.renderer.ui.events.NavigationEvent; */ public class MenuGeneratorsTerrainEditing { - static Window terrainEditingSidePanelWindow; static Window entitySelectionWindow; - //text input - static final int TEXT_INPUT_HEIGHT = 50; - static final int TEXT_INPUT_WIDTH = 200; - - //single voxel button - static final int VOXEL_BUTTON_WIDTH = 90; - static final int VOXEL_BUTTON_HEIGHT = 90; - static final int VOXEL_BUTTON_TEXTURE_DIM = 70; - static final int MARGIN_EACH_SIDE = 5; - - //voxel selection - static final int VOXEL_SCROLLABLE_WIDTH = VOXEL_BUTTON_WIDTH * 5; - static final int VOXEL_SCROLLABLE_HEIGHT = VOXEL_BUTTON_HEIGHT * 5; - //width of the side panel static final int WINDOW_WIDTH = 550; static final int WINDOW_HEIGHT = 550; @@ -61,7 +33,7 @@ public class MenuGeneratorsTerrainEditing { */ public static Window createVoxelTypeSelectionPanel(){ //setup window - terrainEditingSidePanelWindow = Window.create(Globals.renderingEngine.getOpenGLState(), 0, 0, WINDOW_WIDTH, WINDOW_HEIGHT, true); + Window terrainEditingSidePanelWindow = Window.create(Globals.renderingEngine.getOpenGLState(), 0, 0, WINDOW_WIDTH, WINDOW_HEIGHT, true); terrainEditingSidePanelWindow.setParentAlignContent(YogaAlignment.Center); terrainEditingSidePanelWindow.setParentJustifyContent(YogaJustification.Center); terrainEditingSidePanelWindow.setParentAlignItem(YogaAlignment.Center); @@ -79,120 +51,15 @@ public class MenuGeneratorsTerrainEditing { }}); //attach scrollable after search input for organzation purposes - terrainEditingSidePanelWindow.addChild(createVoxelTypeSelectionPanel((VoxelType type) -> { + terrainEditingSidePanelWindow.addChild(VoxelSelectionPanel.createVoxelTypeSelectionPanel((VoxelType type) -> { Globals.clientSelectedVoxelType = type; - })); + })); Globals.signalSystem.post(SignalType.YOGA_APPLY,terrainEditingSidePanelWindow); return terrainEditingSidePanelWindow; } - - /** - * Creates the level editor side panel top view - * @return - */ - public static Div createVoxelTypeSelectionPanel(Consumer onSelectType){ - //setup window - Div rVal = Div.createDiv(); - rVal.setAlignContent(YogaAlignment.Center); - rVal.setAlignItems(YogaAlignment.Center); - rVal.setJustifyContent(YogaJustification.Center); - rVal.setFlexDirection(YogaFlexDirection.Column); - - //scrollable that contains all the voxel types - VirtualScrollable scrollable = new VirtualScrollable(VOXEL_SCROLLABLE_WIDTH, VOXEL_SCROLLABLE_HEIGHT); - scrollable.setFlexDirection(YogaFlexDirection.Column); - scrollable.setAlignItems(YogaAlignment.Start); - - //search input - TextInput searchInput = TextInput.createTextInput(); - searchInput.setWidth(TEXT_INPUT_WIDTH); - searchInput.setMinWidth(TEXT_INPUT_WIDTH); - searchInput.setMinHeight(20); - searchInput.setOnPress(new KeyboardEventCallback() {public boolean execute(KeyboardEvent event){ - boolean rVal = searchInput.defaultKeyHandling(event); - fillInVoxelSelectors(scrollable, searchInput.getText(), onSelectType); - return rVal; - }}); - rVal.addChild(searchInput); - - - //attach scrollable after search input for organzation purposes - rVal.addChild(scrollable); - - //final step - fillInVoxelSelectors(scrollable, searchInput.getText(), onSelectType); - - return rVal; - } - - /** - * Fills in the voxels to display based on the contents of the search string - * @param scrollable the scrollable to drop selection buttons in to - * @param searchString the string to search based on - */ - static void fillInVoxelSelectors(VirtualScrollable scrollable, String searchString, Consumer onSelectType){ - scrollable.clearChildren(); - VoxelData voxelData = Globals.gameConfigCurrent.getVoxelData(); - List matchingVoxels = voxelData.getTypes().stream().filter((type)->type.getName().toLowerCase().contains(searchString.toLowerCase())).toList(); - Div currentRow = null; - int incrementer = 0; - //generate voxel buttons - for(VoxelType type : matchingVoxels){ - if(incrementer % 4 == 0){ - currentRow = Div.createRow(); - currentRow.setJustifyContent(YogaJustification.Evenly); - scrollable.addChild(currentRow); - } - Div containerDiv = Div.createDiv(); - containerDiv.setMinWidthPercent(25.0f); - currentRow.addChild(containerDiv); - - Button newButton = new Button(); - newButton.setAlignItems(YogaAlignment.Center); - //dimensions - newButton.setMinWidth(VOXEL_BUTTON_WIDTH); - newButton.setMinHeight(VOXEL_BUTTON_HEIGHT); - //margin - newButton.setMarginBottom(MARGIN_EACH_SIDE); - newButton.setMarginLeft(MARGIN_EACH_SIDE); - newButton.setMarginRight(MARGIN_EACH_SIDE); - newButton.setMarginTop(MARGIN_EACH_SIDE); - //label - Label voxelLabel = Label.createLabel(type.getName()); - //icon/model - ImagePanel texturePanel = ImagePanel.createImagePanel(type.getTexture()); - if(type.getTexture() != null){ - Globals.assetManager.addTexturePathtoQueue(type.getTexture()); - } - texturePanel.setWidth(VOXEL_BUTTON_TEXTURE_DIM); - texturePanel.setHeight(VOXEL_BUTTON_TEXTURE_DIM); - texturePanel.setMarginBottom(MARGIN_EACH_SIDE); - texturePanel.setMarginLeft(MARGIN_EACH_SIDE); - texturePanel.setMarginRight(MARGIN_EACH_SIDE); - texturePanel.setMarginTop(MARGIN_EACH_SIDE); - newButton.addChild(texturePanel); - texturePanel.setAlignSelf(YogaAlignment.Center); - //button handling - newButton.addChild(voxelLabel); - newButton.setOnClick(new ClickEventCallback() {public boolean execute(ClickEvent event){ - //set voxel type to this type - onSelectType.accept(type); - Globals.clientSelectedVoxelType = type; - return false; - }}); - containerDiv.addChild(newButton); - incrementer++; - } - for(int i = incrementer; i % 4 != 0; i++){ - Div spacerDiv = Div.createDiv(); - spacerDiv.setMinWidthPercent(25.0f); - currentRow.addChild(spacerDiv); - } - } - /** * Creates the entity type selection window * @return diff --git a/src/main/java/electrosphere/client/ui/menu/mainmenu/MenuCharacterCreation.java b/src/main/java/electrosphere/client/ui/menu/mainmenu/MenuCharacterCreation.java new file mode 100644 index 00000000..8a7aea12 --- /dev/null +++ b/src/main/java/electrosphere/client/ui/menu/mainmenu/MenuCharacterCreation.java @@ -0,0 +1,72 @@ +package electrosphere.client.ui.menu.mainmenu; + +import electrosphere.client.ui.components.CharacterCustomizer; +import electrosphere.client.ui.menu.WindowUtils; +import electrosphere.engine.Globals; +import electrosphere.entity.types.creature.CreatureTemplate; +import electrosphere.renderer.ui.elements.Button; +import electrosphere.renderer.ui.elements.FormElement; +import electrosphere.renderer.ui.elements.StringCarousel; +import electrosphere.renderer.ui.elementtypes.Element; +import electrosphere.renderer.ui.events.ValueChangeEvent; + +/** + * The menu for character creation + */ +public class MenuCharacterCreation { + + /** + * The currently selected race + */ + static String selectedRace = ""; + + /** + * Creates the menu for selecting/creating a character + * @return The menu element + */ + public static Element createCharacterSelectionWindow(){ + FormElement rVal = new FormElement(); + + //button (create) + rVal.addChild(Button.createButton("Create Character", () -> { + WindowUtils.replaceMainMenuContents(MenuCharacterCreation.createRaceSelectionMenu()); + })); + + return rVal; + } + + /** + * Creates the menu for selecting a character's race + * @return The menu element + */ + public static Element createRaceSelectionMenu(){ + FormElement rVal = new FormElement(); + + //select race + rVal.addChild(StringCarousel.create(Globals.gameConfigCurrent.getCreatureTypeLoader().getPlayableRaces(), (ValueChangeEvent event) -> { + selectedRace = event.getAsString(); + })); + + selectedRace = Globals.gameConfigCurrent.getCreatureTypeLoader().getPlayableRaces().get(0); + + //button (create) + rVal.addChild(Button.createButton("Confirm", () -> { + WindowUtils.replaceMainMenuContents(MenuCharacterCreation.createCharacterCustomizationMenu()); + })); + + return rVal; + } + + /** + * Creates the menu for customizing the character's appearance + * @return The menu element + */ + public static Element createCharacterCustomizationMenu(){ + FormElement rVal = new FormElement(); + + rVal.addChild(CharacterCustomizer.createCharacterCustomizerPanel(selectedRace, (CreatureTemplate template) -> {})); + + return rVal; + } + +} diff --git a/src/main/java/electrosphere/client/ui/menu/mainmenu/MenuGeneratorsLevelEditor.java b/src/main/java/electrosphere/client/ui/menu/mainmenu/MenuGeneratorsLevelEditor.java index dac7877e..c1f092e0 100644 --- a/src/main/java/electrosphere/client/ui/menu/mainmenu/MenuGeneratorsLevelEditor.java +++ b/src/main/java/electrosphere/client/ui/menu/mainmenu/MenuGeneratorsLevelEditor.java @@ -3,8 +3,8 @@ package electrosphere.client.ui.menu.mainmenu; import java.util.List; import electrosphere.client.ui.components.InputMacros; +import electrosphere.client.ui.components.VoxelSelectionPanel; import electrosphere.client.ui.menu.WindowUtils; -import electrosphere.client.ui.menu.ingame.MenuGeneratorsTerrainEditing; import electrosphere.engine.Globals; import electrosphere.engine.assetmanager.AssetDataStrings; import electrosphere.engine.loadingthreads.LoadingThread; @@ -194,7 +194,7 @@ public class MenuGeneratorsLevelEditor { }, DEFAULT_GRID_SIZE / (float)GriddedDataCellManager.MAX_GRID_SIZE) ); sceneFile.getRealmDescriptor().setGriddedRealmSize(DEFAULT_GRID_SIZE); - griddedRealmControls.addChild(MenuGeneratorsTerrainEditing.createVoxelTypeSelectionPanel((VoxelType type) -> { + griddedRealmControls.addChild(VoxelSelectionPanel.createVoxelTypeSelectionPanel((VoxelType type) -> { sceneFile.getRealmDescriptor().setBaseVoxel(type.getId()); })); } @@ -205,11 +205,13 @@ public class MenuGeneratorsLevelEditor { // //Create level button // - rVal.addChild(Button.createButton("Create Level", () -> { + Button createButton = Button.createButton("Create Level", () -> { //launch level editor LoadingThread loadingThread = new LoadingThread(LoadingThreadType.LEVEL_EDITOR, inFlightLevel); Globals.threadManager.start(loadingThread); - }).setOnClickAudio(AssetDataStrings.UI_TONE_BUTTON_TITLE)); + }).setOnClickAudio(AssetDataStrings.UI_TONE_BUTTON_TITLE); + createButton.setAlignSelf(YogaAlignment.Center); + rVal.addChild(createButton); return rVal; diff --git a/src/main/java/electrosphere/client/ui/menu/mainmenu/MenuGeneratorsMultiplayer.java b/src/main/java/electrosphere/client/ui/menu/mainmenu/MenuGeneratorsMultiplayer.java index 3c38309b..065bd4ea 100644 --- a/src/main/java/electrosphere/client/ui/menu/mainmenu/MenuGeneratorsMultiplayer.java +++ b/src/main/java/electrosphere/client/ui/menu/mainmenu/MenuGeneratorsMultiplayer.java @@ -4,6 +4,7 @@ import electrosphere.client.ui.components.CharacterCustomizer; import electrosphere.client.ui.menu.MenuGenerators; import electrosphere.client.ui.menu.WindowUtils; import electrosphere.engine.Globals; +import electrosphere.entity.types.creature.CreatureTemplate; import electrosphere.renderer.ui.elements.Button; import electrosphere.renderer.ui.elements.FormElement; import electrosphere.renderer.ui.elements.Label; @@ -52,7 +53,7 @@ public class MenuGeneratorsMultiplayer { createButton.addChild(createLabel); rVal.addChild(createButton); createButton.setOnClick(new ClickableElement.ClickEventCallback(){public boolean execute(ClickEvent event){ - WindowUtils.replaceMainMenuContents(CharacterCustomizer.createCharacterCustomizerPanel(selectedRace)); + WindowUtils.replaceMainMenuContents(CharacterCustomizer.createCharacterCustomizerPanel(selectedRace, (CreatureTemplate template) -> {})); return false; }}); diff --git a/src/main/java/electrosphere/client/ui/menu/mainmenu/MenuGeneratorsUITesting.java b/src/main/java/electrosphere/client/ui/menu/mainmenu/MenuGeneratorsUITesting.java index 0e2cc124..e651945f 100644 --- a/src/main/java/electrosphere/client/ui/menu/mainmenu/MenuGeneratorsUITesting.java +++ b/src/main/java/electrosphere/client/ui/menu/mainmenu/MenuGeneratorsUITesting.java @@ -13,8 +13,8 @@ import electrosphere.client.ui.components.EquipmentInventoryPanel; import electrosphere.client.ui.components.InputMacros; import electrosphere.client.ui.components.NaturalInventoryPanel; import electrosphere.client.ui.components.SpawnSelectionPanel; +import electrosphere.client.ui.components.VoxelSelectionPanel; import electrosphere.client.ui.menu.WindowUtils; -import electrosphere.client.ui.menu.ingame.MenuGeneratorsTerrainEditing; import electrosphere.engine.Globals; import electrosphere.engine.assetmanager.AssetDataStrings; import electrosphere.entity.Entity; @@ -22,6 +22,7 @@ import electrosphere.entity.EntityCreationUtils; import electrosphere.entity.state.inventory.InventoryUtils; import electrosphere.entity.state.inventory.RelationalInventoryState; import electrosphere.entity.state.inventory.UnrelationalInventoryState; +import electrosphere.entity.types.creature.CreatureTemplate; import electrosphere.game.data.common.CommonEntityType; import electrosphere.game.data.creature.type.equip.EquipPoint; import electrosphere.game.data.voxel.VoxelType; @@ -123,7 +124,7 @@ public class MenuGeneratorsUITesting { formEl.addChild(Button.createButton("test", () -> {})); } break; case "CharacterCustomizer": { - formEl.addChild(CharacterCustomizer.createCharacterCustomizerPanel("human")); + formEl.addChild(CharacterCustomizer.createCharacterCustomizerPanel("human", (CreatureTemplate template) ->{})); } break; case "NaturalInventoryPanel": { Entity ent = EntityCreationUtils.TEST_createEntity(); @@ -144,7 +145,7 @@ public class MenuGeneratorsUITesting { formEl.addChild(EquipmentInventoryPanel.createEquipmentInventoryPanel(ent)); } break; case "VoxelPicker": { - formEl.addChild(MenuGeneratorsTerrainEditing.createVoxelTypeSelectionPanel((VoxelType voxelType) -> { + formEl.addChild(VoxelSelectionPanel.createVoxelTypeSelectionPanel((VoxelType voxelType) -> { System.out.println(voxelType.getName()); })); } break; diff --git a/src/main/java/electrosphere/engine/Main.java b/src/main/java/electrosphere/engine/Main.java index 0c373b55..5066dd37 100644 --- a/src/main/java/electrosphere/engine/Main.java +++ b/src/main/java/electrosphere/engine/Main.java @@ -300,6 +300,13 @@ public class Main { /// Globals.scriptEngine.handleAllSignals(); + /// + /// S C R I P T E N G I N E + /// + if(Globals.RUN_SCRIPTS && Globals.scriptEngine != null){ + Globals.scriptEngine.scanScriptDir(); + } + /// /// diff --git a/src/main/java/electrosphere/engine/loadingthreads/ClientLoading.java b/src/main/java/electrosphere/engine/loadingthreads/ClientLoading.java index e9d54391..93c50375 100644 --- a/src/main/java/electrosphere/engine/loadingthreads/ClientLoading.java +++ b/src/main/java/electrosphere/engine/loadingthreads/ClientLoading.java @@ -14,7 +14,7 @@ import electrosphere.client.terrain.cells.ClientDrawCellManager; import electrosphere.client.ui.menu.MenuGenerators; import electrosphere.client.ui.menu.WindowStrings; import electrosphere.client.ui.menu.WindowUtils; -import electrosphere.client.ui.menu.mainmenu.MenuGeneratorsMultiplayer; +import electrosphere.client.ui.menu.mainmenu.MenuCharacterCreation; import electrosphere.controls.ControlHandler; import electrosphere.engine.Globals; import electrosphere.engine.assetmanager.AssetDataStrings; @@ -45,6 +45,10 @@ public class ClientLoading { static final int DRAW_CELL_EXPECTED_MINIMUM_FRAMES_TO_INIT = 10; + /** + * Loads the race data from the server + * @param params no params + */ protected static void loadCharacterServer(Object[] params){ WindowUtils.recursiveSetVisible(Globals.elementService.getWindow(WindowStrings.WINDOW_MENU_MAIN), false); WindowUtils.replaceMainMenuContents(MenuGenerators.createEmptyMainMenu()); @@ -63,7 +67,7 @@ public class ClientLoading { //once we have them, bring up the character creation interface //init character creation window //eventually should replace with at ui to select an already created character or create a new one - WindowUtils.replaceMainMenuContents(MenuGeneratorsMultiplayer.createMultiplayerCharacterCreationWindow()); + WindowUtils.replaceMainMenuContents(MenuCharacterCreation.createCharacterSelectionWindow()); //make loading dialog disappear WindowUtils.recursiveSetVisible(Globals.elementService.getWindow(WindowStrings.WINDOW_LOADING), false); //make character creation window visible diff --git a/src/main/java/electrosphere/entity/scene/SceneLoader.java b/src/main/java/electrosphere/entity/scene/SceneLoader.java index 442a67c4..6103b2f3 100644 --- a/src/main/java/electrosphere/entity/scene/SceneLoader.java +++ b/src/main/java/electrosphere/entity/scene/SceneLoader.java @@ -125,8 +125,8 @@ public class SceneLoader { //load scripts if(!isLevelEditor && file.getInitScriptPath() != null){ Realm finalRealm = realm; - Globals.scriptEngine.executeSynchronously(() -> { - int sceneInstanceId = Globals.scriptEngine.initScene(file.getInitScriptPath()); + Globals.scriptEngine.getScriptContext().executeSynchronously(() -> { + int sceneInstanceId = Globals.scriptEngine.getScriptContext().initScene(file.getInitScriptPath()); finalRealm.setSceneInstanceId(sceneInstanceId); }); } diff --git a/src/main/java/electrosphere/renderer/ui/elements/Button.java b/src/main/java/electrosphere/renderer/ui/elements/Button.java index fa1c7ace..fbf430c9 100644 --- a/src/main/java/electrosphere/renderer/ui/elements/Button.java +++ b/src/main/java/electrosphere/renderer/ui/elements/Button.java @@ -22,9 +22,17 @@ import electrosphere.renderer.ui.events.FocusEvent; import electrosphere.renderer.ui.events.HoverEvent; import electrosphere.renderer.ui.events.MouseEvent; +/** + * A button element + */ public class Button extends StandardContainerElement implements DrawableElement, FocusableElement, ClickableElement, HoverableElement { - static Vector3f color = new Vector3f(1.0f); + static Vector3f COLOR_DEFAULT = new Vector3f(1.0f); + + /** + * The color of the backing element + */ + Vector3f color = new Vector3f(COLOR_DEFAULT); Vector3f boxPosition = new Vector3f(); Vector3f boxDimensions = new Vector3f(); @@ -302,5 +310,13 @@ public class Button extends StandardContainerElement implements DrawableElement, this.audioPathOnClick = audioPath; return this; } + + /** + * Sets the background color of this element + * @param color The color + */ + public void setColor(Vector3f color){ + this.color.set(color); + } } diff --git a/src/main/java/electrosphere/renderer/ui/font/FontUtils.java b/src/main/java/electrosphere/renderer/ui/font/FontUtils.java index 4b438806..ad207027 100644 --- a/src/main/java/electrosphere/renderer/ui/font/FontUtils.java +++ b/src/main/java/electrosphere/renderer/ui/font/FontUtils.java @@ -5,18 +5,12 @@ import electrosphere.renderer.model.Material; import electrosphere.renderer.texture.Texture; import java.awt.image.BufferedImage; -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; import java.awt.RenderingHints; import java.awt.FontMetrics; import java.awt.Graphics2D; import java.util.LinkedList; import java.util.List; -import javax.imageio.ImageIO; - - /** * Utilities for loading fonts @@ -140,15 +134,15 @@ public class FontUtils { // AffineTransformOp operation = new AffineTransformOp(transform, AffineTransformOp.TYPE_NEAREST_NEIGHBOR); // image = operation.filter(image, null); - try { - File imgFile = new File("./.testcache/testimg.png"); - imgFile.delete(); - imgFile.getParentFile().mkdirs(); - ImageIO.write(image, "png", Files.newOutputStream(imgFile.toPath())); - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } + // try { + // File imgFile = new File("./.testcache/testimg.png"); + // imgFile.delete(); + // imgFile.getParentFile().mkdirs(); + // ImageIO.write(image, "png", Files.newOutputStream(imgFile.toPath())); + // } catch (IOException e) { + // // TODO Auto-generated catch block + // e.printStackTrace(); + // } //create material with new font image diff --git a/src/main/java/electrosphere/script/ScriptContext.java b/src/main/java/electrosphere/script/ScriptContext.java new file mode 100644 index 00000000..791e9c1b --- /dev/null +++ b/src/main/java/electrosphere/script/ScriptContext.java @@ -0,0 +1,409 @@ +package electrosphere.script; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.concurrent.locks.ReentrantLock; + +import org.graalvm.polyglot.Context; +import org.graalvm.polyglot.Engine; +import org.graalvm.polyglot.HostAccess; +import org.graalvm.polyglot.PolyglotException; +import org.graalvm.polyglot.Source; +import org.graalvm.polyglot.Source.Builder; +import org.graalvm.polyglot.Value; + +import electrosphere.engine.Globals; +import electrosphere.logger.LoggerInterface; +import electrosphere.util.FileUtils; + +/** + * A context for executing scripts + */ +public class ScriptContext { + + //the default namespaces for + public static String SCRIPT_NAMESPACE_ENGINE = "engine"; //namespace for the engine functions exposed to the script engine + public static String SCRIPT_NAMESPACE_SCRIPT = "script"; //namespace for the core typescript functionsw + public static String SCRIPT_NAMESPACE_SCENE = "scene"; //namespace for the current scene + + //the graal context + Context context; + + //used to build source objects + Builder builder; + + //the javascript object that stores values + Value topLevelValue; + + //the object that contains all host values accessible to javascript land + Value hostObject; + + //the engine object + Value engineObject; + + /** + * The hook manager + */ + Value hookManager; + + /** + * The parent script engine + */ + ScriptEngine parent; + + /** + * Locks the script engine to enforce synchronization + */ + ReentrantLock lock = new ReentrantLock(); + + /** + * Initializes the context + * @param engine + */ + public void init(ScriptEngine scriptEngine){ + //register parent + this.parent = scriptEngine; + + + //create engine with flag to disable warning + Engine engine = Engine.newBuilder() + .option("engine.WarnInterpreterOnly", "false") + .build(); + + //Create the rules for guest accessing the host environment + HostAccess accessRules = HostAccess.newBuilder(HostAccess.EXPLICIT) + .allowArrayAccess(true) + .build(); + + //create context + context = Context.newBuilder("js") + .allowNativeAccess(false) + .allowHostAccess(accessRules) + .engine(engine) + .build(); + //save the js bindings object + topLevelValue = context.getBindings("js"); + + //put host members into environment + this.putTopLevelValue("loggerScripts",LoggerInterface.loggerScripts); + + //load all files required to start the engine + for(String fileToLoad : ScriptEngine.filesToLoadOnInit){ + this.loadDependency(fileToLoad); + } + + //register engine files + this.registerFile("/Scripts/engine/engine-init.ts"); + } + + /** + * Logic to run after initializing + */ + public void postInit(){ + //run script for engine init + this.requireModule("/Scripts/engine/engine-init.ts"); + + //get the engine object + engineObject = topLevelValue.getMember("REQUIRE_CACHE").getMember("/Scripts/engine/engine-init.js").getMember("exports").getMember("engine"); + hookManager = engineObject.getMember("hookManager"); + + //define host members + this.defineHostMembers(); + + //init on script side + this.invokeModuleFunction("/Scripts/engine/engine-init.ts","ENGINE_onInit"); + } + + + /** + * Stores a variable at the top level of the js bindings + * @param valueName The name of the variable (ie the name of the variable) + * @param value The value that is stored at that variable + */ + public void putTopLevelValue(String valueName, Object value){ + topLevelValue.putMember(valueName, value); + } + + /** + * Gets a top level value from the script engine + * @param valueName The name of the variable + * @return The value of the variable + */ + public Value getTopLevelValue(String valueName){ + return topLevelValue.getMember(valueName); + } + + /** + * Removes a top level member from the javascript context + * @param valueName The name of the top level member + * @return true if successfully removed, false otherwise + */ + public boolean removeTopLevelValue(String valueName){ + return topLevelValue.removeMember(valueName); + } + + /** + * Loads a script from disk + * @param path The path to the script file + */ + void loadDependency(String path){ + String content; + Source source = null; + try { + content = FileUtils.getAssetFileAsString(path); + source = Source.create("js",content); + context.eval(source); + } catch (IOException e) { + LoggerInterface.loggerScripts.ERROR("FAILED TO LOAD SCRIPT", e); + } catch (PolyglotException e){ + if(source != null){ + LoggerInterface.loggerScripts.WARNING("Source language: " + source.getLanguage()); + } + LoggerInterface.loggerScripts.ERROR("Script error", e); + e.printStackTrace(); + } + } + + /** + * Prints the content of a file + * @param path The filepath of the script + */ + public void printScriptSource(String path){ + invokeMemberFunction("COMPILER", "printSource", path); + } + + /** + * Gets the contents of a file in the virtual filepath + * @param path The virtual filepath + * @return The contents of that file if it exists, null otherwise + */ + public String getVirtualFileContent(String path){ + String rVal = null; + Value compiler = this.topLevelValue.getMember("COMPILER"); + Value fileMap = compiler.getMember("fileMap"); + Value virtualFile = fileMap.getMember(path); + rVal = virtualFile.getMember("content").asString(); + return rVal; + } + + /** + * Registers a file with the scripting engine to be compiled into the full binary + * @param path The path to the script file + */ + protected boolean registerFile(String path){ + String content; + try { + content = FileUtils.getAssetFileAsString(path); + Value dependentFilesValue = this.invokeMemberFunction("COMPILER", "registerFile", path, content); + // + //register dependent files if necessary + long dependentFilesCount = dependentFilesValue.getArraySize(); + if(dependentFilesCount > 0){ + for(int i = 0; i < dependentFilesCount; i++){ + String dependentFilePath = dependentFilesValue.getArrayElement(i).asString(); + boolean shouldRegister = true; + for(String ignorePath : ScriptEngine.registerIgnores){ + if(ignorePath.equals(dependentFilePath)){ + shouldRegister = false; + } + } + if(shouldRegister){ + LoggerInterface.loggerScripts.INFO("[HOST - Script Engine] Should register file " + dependentFilePath); + this.registerFile(dependentFilePath); + } else { + LoggerInterface.loggerScripts.DEBUG("[HOST - Script Engine] Skipping ignorepath file " + dependentFilePath); + } + } + } + } catch (IOException e) { + LoggerInterface.loggerScripts.ERROR("FAILED TO LOAD SCRIPT", e); + return false; + } + return true; + } + + /** + * Compiles the project + */ + protected void compile(){ + //actually compile + this.invokeMemberFunction("COMPILER", "run"); + //TODO:update local cache + Value fileMap = this.topLevelValue.getMember("COMPILER").getMember("fileMap"); + for(String key : fileMap.getMemberKeys()){ + Value fileData = fileMap.getMember(key); + String content = fileData.getMember("content").asString(); + String cacheFilePath = ScriptEngine.TS_SOURCE_CACHE_DIR + key; + File toWriteFile = new File(cacheFilePath); + + //make sure all containing folders exist + try { + Files.createDirectories(toWriteFile.getParentFile().toPath()); + } catch (IOException e) { + LoggerInterface.loggerFileIO.ERROR(e); + } + + //write the actual file + try { + Files.writeString(toWriteFile.toPath(), content); + } catch (IOException e) { + LoggerInterface.loggerFileIO.ERROR(e); + } + } + } + + /** + * Recompiles the scripting engine + */ + protected void recompile(Runnable onCompletion){ + Thread recompileThread = new Thread(() -> { + Globals.scriptEngine.getScriptContext().executeSynchronously(() -> { + Globals.scriptEngine.initScripts(); + }); + if(onCompletion != null){ + onCompletion.run(); + } + }); + recompileThread.setName("Recompile Script Engine"); + recompileThread.start(); + } + + /** + * Initializes a scene script + * @param scenePath The scene's init script path + * @return The id assigned to the scene instance from the script-side + */ + public int initScene(String scenePath){ + //add files to virtual filesystem in script engine + registerFile(scenePath); + //load scene from javascript side + Value sceneLoader = this.engineObject.getMember("sceneLoader"); + Value loadFunc = sceneLoader.getMember("loadScene"); + Value result = loadFunc.execute(scenePath); + return result.asInt(); + } + + + /** + * Calls a function defined in the global scope with the arguments provided + * @param functionName The function name + * @param args The arguments + */ + public Value invokeFunction(String functionName, Object... args){ + LoggerInterface.loggerScripts.DEBUG("Host execute: " + functionName); + Value function = topLevelValue.getMember(functionName); + if(function != null){ + return function.execute(args); + } else { + LoggerInterface.loggerScripts.WARNING("Failed to invoke function " + functionName); + } + return null; + } + + /** + * Calls a function on a child of the top level member + * @param memberName The name of the child + * @param functionName The name of the function + * @param args The arguments for the function + * @return The value from the function call + */ + public Value invokeMemberFunction(String memberName, String functionName, Object ... args){ + LoggerInterface.loggerScripts.DEBUG("Host execute: " + functionName); + Value childMember = topLevelValue.getMember(memberName); + Value function = childMember.getMember(functionName); + if(function != null){ + return function.execute(args); + } else { + LoggerInterface.loggerScripts.WARNING("Failed to invoke function " + functionName); + } + return null; + } + + /** + * Invokes a function defined in a file + * @param filePath The file the function is defined in + * @param functionName The function's name + * @param args The args to pass into the function + */ + public void invokeModuleFunction(String filePath, String functionName, Object ... args){ + Value filePathRaw = invokeFunction("FILE_RESOLUTION_getFilePath",filePath); + Value requireCache = topLevelValue.getMember("REQUIRE_CACHE"); + Value module = requireCache.getMember(filePathRaw.asString()); + Value exports = module.getMember("exports"); + Value function = exports.getMember(functionName); + if(function != null && function.canExecute()){ + function.execute(args); + } else { + LoggerInterface.loggerScripts.WARNING("Failed to invoke function " + functionName); + } + } + + /** + * Requires a module into the global space + * @param filePath The filepath of the module + */ + public void requireModule(String filePath){ + invokeFunction("require", filePath); + } + + /** + * Invokes a function on a member of arbitrary depth on the engine object + * @param memberName The member name + * @param functionName The function's name + * @param className The class of the expected return value + * @param args The args to pass to the function + * @return The results of the invocation or null if there was no result + */ + public Value invokeEngineMember(String memberName, String functionName, Object ... args){ + Value member = this.engineObject.getMember(memberName); + if(member == null){ + throw new Error("Member is null!"); + } + Value function = member.getMember(functionName); + if(function == null || !function.canExecute()){ + throw new Error("Function is not executable! " + function); + } + Value executionResult = function.execute(args); + if(executionResult == null){ + return null; + } + return executionResult; + } + + /** + * Executes some code synchronously that requires script engine access + * @param function The function + */ + public void executeSynchronously(Runnable function){ + lock.lock(); + function.run(); + lock.unlock(); + } + + /** + * Defines host members within javascript context + */ + protected void defineHostMembers(){ + hostObject = topLevelValue.getMember("HOST_ACCESS"); + //give guest access to static classes + Value classes = engineObject.getMember("classes"); + for(Object[] currentClass : ScriptEngine.staticClasses){ + classes.putMember((String)currentClass[0], currentClass[1]); + } + //give access to script engine instance + hostObject.putMember("scriptEngine", this); + } + + /** + * Fires a signal on a given scene + * @param signal The signal name + * @param sceneInstanceId The script-side instanceid of the scene + * @param args The arguments to accompany the signal invocation + */ + public void fireSignal(String signal, int sceneInstanceId, Object ... args){ + Value fireSignal = this.hookManager.getMember("fireSignal"); + fireSignal.execute(sceneInstanceId,signal,args); + } + +} diff --git a/src/main/java/electrosphere/script/ScriptEngine.java b/src/main/java/electrosphere/script/ScriptEngine.java index 1ce9beb6..d842d1ce 100644 --- a/src/main/java/electrosphere/script/ScriptEngine.java +++ b/src/main/java/electrosphere/script/ScriptEngine.java @@ -2,16 +2,23 @@ package electrosphere.script; import java.io.File; import java.io.IOException; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.StandardWatchEventKinds; +import java.nio.file.WatchEvent; +import java.nio.file.WatchKey; +import java.nio.file.WatchService; +import java.nio.file.attribute.BasicFileAttributes; +import java.security.NoSuchAlgorithmException; import java.util.HashMap; +import java.util.List; import java.util.Map; -import java.util.concurrent.locks.ReentrantLock; -import org.graalvm.polyglot.Context; -import org.graalvm.polyglot.Engine; -import org.graalvm.polyglot.HostAccess; -import org.graalvm.polyglot.PolyglotException; import org.graalvm.polyglot.Source; -import org.graalvm.polyglot.Source.Builder; import org.graalvm.polyglot.Value; import electrosphere.client.script.ScriptClientVoxelUtils; @@ -29,55 +36,60 @@ import electrosphere.util.FileUtils; import electrosphere.util.math.SpatialMathUtils; /** - * Interface for executing scripts in the game engine + * Handles the actual file loading of script files */ public class ScriptEngine extends SignalServiceImpl { - //the default namespaces for - public static String SCRIPT_NAMESPACE_ENGINE = "engine"; //namespace for the engine functions exposed to the script engine - public static String SCRIPT_NAMESPACE_SCRIPT = "script"; //namespace for the core typescript functionsw - public static String SCRIPT_NAMESPACE_SCENE = "scene"; //namespace for the current scene + /** + * The directory with all script source files + */ + public static final String TS_SOURCE_DIR = "./assets/Scripts"; + + /** + * The typescript cache dir + */ + public static final String TS_CACHE_DIR = "./.cache/tscache"; + + /** + * Directory that should contain all ts source dirs + */ + public static final String TS_SOURCE_CACHE_DIR = TS_CACHE_DIR + "/src"; /** * The id for firing signals globally */ public static final int GLOBAL_SCENE = -1; - //the graal context - Context context; - - //used to build source objects - Builder builder; - //the map of script filepaths to parsed, in-memory scripts Map sourceMap; - //the javascript object that stores values - Value topLevelValue; - - //the object that contains all host values accessible to javascript land - Value hostObject; - - //the engine object - Value engineObject; + /** + * Stores all loaded files' md5 checksums + */ + Map fileChecksumMap = new HashMap(); /** - * The hook manager + * The script context */ - Value hookManager; + ScriptContext scriptContext = new ScriptContext(); + + /** + * The file system object + */ + FileSystem fs; + + /** + * The watch service + */ + WatchService watchService; /** * Tracks the initialization status of the script engine */ boolean initialized = false; - /** - * Locks the script engine to enforce synchronization - */ - ReentrantLock lock = new ReentrantLock(); - //The files that are loaded on init to bootstrap the script engine - static final String[] filesToLoadOnInit = new String[]{ + public static final String[] filesToLoadOnInit = new String[]{ //polyfills "Scripts/compiler/require_polyfill.js", @@ -93,13 +105,13 @@ public class ScriptEngine extends SignalServiceImpl { /** * List of files that are ignored when registering new files */ - static final String[] registerIgnores = new String[]{ + public static final String[] registerIgnores = new String[]{ "/Scripts/compiler/host_access.ts", }; //The classes that will be provided to the scripting engine //https://stackoverflow.com/a/65942034 - static final Object[][] staticClasses = new Object[][]{ + public static final Object[][] staticClasses = new Object[][]{ {"mathUtils",SpatialMathUtils.class}, {"simulation",Main.class}, {"tutorialUtils",TutorialMenus.class}, @@ -111,7 +123,7 @@ public class ScriptEngine extends SignalServiceImpl { }; //singletons from the host that are provided to the javascript context - static final Object[][] hostSingletops = new Object[][]{ + public static final Object[][] hostSingletops = new Object[][]{ {"timekeeper",Globals.timekeeper}, {"currentPlayer",Globals.clientPlayer}, {"loggerScripts",LoggerInterface.loggerScripts}, @@ -123,6 +135,29 @@ public class ScriptEngine extends SignalServiceImpl { */ public ScriptEngine(){ super("ScriptEngine"); + sourceMap = new HashMap(); + this.fs = FileSystems.getDefault(); + try { + this.watchService = fs.newWatchService(); + } catch (IOException e) { + LoggerInterface.loggerFileIO.ERROR(e); + } + //register all source directories + try { + Files.walkFileTree(new File(TS_SOURCE_DIR).toPath(), new SimpleFileVisitor(){ + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attr) throws IOException { + dir.register( + watchService, + new WatchEvent.Kind[]{StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE} + ); + return FileVisitResult.CONTINUE; + } + }); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } } /** @@ -130,365 +165,113 @@ public class ScriptEngine extends SignalServiceImpl { */ public void initScripts(){ //init datastructures - sourceMap = new HashMap(); initialized = false; - //create engine with flag to disable warning - Engine engine = Engine.newBuilder() - .option("engine.WarnInterpreterOnly", "false") - .build(); + //init script context + scriptContext.init(this); - //Create the rules for guest accessing the host environment - HostAccess accessRules = HostAccess.newBuilder(HostAccess.EXPLICIT) - .allowArrayAccess(true) - .build(); - - //create context - context = Context.newBuilder("js") - .allowNativeAccess(false) - .allowHostAccess(accessRules) - .engine(engine) - .build(); - //save the js bindings object - topLevelValue = context.getBindings("js"); - - //put host members into environment - putTopLevelValue("loggerScripts",LoggerInterface.loggerScripts); - - //load all files required to start the engine - for(String fileToLoad : filesToLoadOnInit){ - loadDependency(fileToLoad); - } - - //register engine files - registerFile("/Scripts/engine/engine-init.ts"); + //read files from cache + boolean readCache = this.initCache(); //compile - compile(); + if(!readCache){ + scriptContext.compile(); + } - //run script for engine init - requireModule("/Scripts/engine/engine-init.ts"); + //post init logic + scriptContext.postInit(); - //get the engine object - engineObject = topLevelValue.getMember("REQUIRE_CACHE").getMember("/Scripts/engine/engine-init.js").getMember("exports").getMember("engine"); - hookManager = engineObject.getMember("hookManager"); - - //define host members - defineHostMembers(); - - //init on script side - invokeModuleFunction("/Scripts/engine/engine-init.ts","ENGINE_onInit"); - - - //call the engine initialization function - // invokeFunction("ENGINE_onInit"); - - - //read scripts into source map - // readScriptsDirectory("/src/main/sql", FileUtils.getAssetFile("/src/main/sql")); - //create bindings - // try { - // String content = FileUtils.getAssetFileAsString("/Scripts/test.js"); - // Source source = Source.create("js", content); - // context.eval(source); - // System.out.println("Evaluated"); - // } catch (IOException e) { - // // TODO Auto-generated catch block - // e.printStackTrace(); - // } initialized = true; } /** - * Stores a variable at the top level of the js bindings - * @param valueName The name of the variable (ie the name of the variable) - * @param value The value that is stored at that variable + * Scans the scripts directory for updates */ - public void putTopLevelValue(String valueName, Object value){ - topLevelValue.putMember(valueName, value); - } - - /** - * Gets a top level value from the script engine - * @param valueName The name of the variable - * @return The value of the variable - */ - public Value getTopLevelValue(String valueName){ - return topLevelValue.getMember(valueName); - } - - /** - * Removes a top level member from the javascript context - * @param valueName The name of the top level member - * @return true if successfully removed, false otherwise - */ - public boolean removeTopLevelValue(String valueName){ - return topLevelValue.removeMember(valueName); - } - - @Deprecated - /** - * Reads the scripts directory - * @param path The - * @param directory - */ - void registerScriptDirectory(String path, File directory){ - if(directory.exists() && directory.isDirectory()){ - File[] children = directory.listFiles(); - for(File childFile : children){ - String qualifiedName = path + "/" + childFile.getName(); - if(childFile.isDirectory()){ - registerScriptDirectory(qualifiedName,childFile); - } else { - //add to source map - registerFile(qualifiedName); - // String content = FileUtils.readFileToString(childFile); - // sourceMap.put(qualifiedName,Source.create("js",content)); + public void scanScriptDir(){ + WatchKey key = null; + while((key = watchService.poll()) != null){ + List> events = key.pollEvents(); + for(WatchEvent event : events){ + if(event.kind() == StandardWatchEventKinds.ENTRY_MODIFY){ + if(event.context() instanceof Path){ + // Path filePath = (Path)event.context(); + // System.out.println(filePath); + } + } else if(event.kind() == StandardWatchEventKinds.ENTRY_CREATE){ + throw new Error("Cannot handle create events yet"); + } else if(event.kind() == StandardWatchEventKinds.ENTRY_DELETE){ + throw new Error("Cannot handle delete events yet"); } } + key.reset(); } } /** - * Loads a script from disk - * @param path The path to the script file + * Gets the script context of the engine + * @return The script context */ - void loadDependency(String path){ - String content; - Source source = null; - try { - content = FileUtils.getAssetFileAsString(path); - source = Source.create("js",content); - sourceMap.put(path,source); - context.eval(source); - } catch (IOException e) { - LoggerInterface.loggerScripts.ERROR("FAILED TO LOAD SCRIPT", e); - } catch (PolyglotException e){ - if(source != null){ - LoggerInterface.loggerScripts.WARNING("Source language: " + source.getLanguage()); + public ScriptContext getScriptContext(){ + return this.scriptContext; + } + + /** + * Makes sure the cache folder exists + * @return true if files were read from cache, false otherwise + */ + private boolean initCache(){ + File tsCache = new File(ScriptEngine.TS_SOURCE_CACHE_DIR); + if(!tsCache.exists()){ + try { + Files.createDirectories(tsCache.toPath()); + } catch (IOException e) { + LoggerInterface.loggerFileIO.ERROR(e); } - LoggerInterface.loggerScripts.ERROR("Script error", e); - e.printStackTrace(); - } - } - - /** - * Prints the content of a file - * @param path The filepath of the script - */ - public void printScriptSource(String path){ - invokeMemberFunction("COMPILER", "printSource", path); - } - - /** - * Gets the contents of a file in the virtual filepath - * @param path The virtual filepath - * @return The contents of that file if it exists, null otherwise - */ - public String getVirtualFileContent(String path){ - String rVal = null; - Value compiler = this.topLevelValue.getMember("COMPILER"); - Value fileMap = compiler.getMember("fileMap"); - Value virtualFile = fileMap.getMember(path); - rVal = virtualFile.getMember("content").asString(); - return rVal; - } - - /** - * Registers a file with the scripting engine to be compiled into the full binary - * @param path The path to the script file - */ - private boolean registerFile(String path){ - String content; - try { - content = FileUtils.getAssetFileAsString(path); - sourceMap.put(path,Source.create("js",content)); - Value dependentFilesValue = invokeMemberFunction("COMPILER", "registerFile",path,content); - // - //register dependent files if necessary - long dependentFilesCount = dependentFilesValue.getArraySize(); - if(dependentFilesCount > 0){ - for(int i = 0; i < dependentFilesCount; i++){ - String dependentFilePath = dependentFilesValue.getArrayElement(i).asString(); - boolean shouldRegister = true; - for(String ignorePath : registerIgnores){ - if(ignorePath.equals(dependentFilePath)){ - shouldRegister = false; - } - } - if(shouldRegister){ - LoggerInterface.loggerScripts.INFO("[HOST - Script Engine] Should register file " + dependentFilePath); - registerFile(dependentFilePath); - } else { - LoggerInterface.loggerScripts.DEBUG("[HOST - Script Engine] Skipping ignorepath file " + dependentFilePath); - } - } - } - } catch (IOException e) { - LoggerInterface.loggerScripts.ERROR("FAILED TO LOAD SCRIPT", e); return false; + } else { + Value fileMap = this.scriptContext.getTopLevelValue("COMPILER").getMember("fileMap"); + this.recursivelyRegisterCachedFiles(tsCache,fileMap,tsCache); + return true; } - return true; } /** - * Compiles the project + * Register files recursively from the current file + * @param tsCache The ts cache file + * @param fileMap The file map on script side + * @param currentDirectory The current directory */ - private void compile(){ - invokeMemberFunction("COMPILER", "run"); - } + private void recursivelyRegisterCachedFiles(File tsCache, Value fileMap, File currentDirectory){ + for(File file : currentDirectory.listFiles()){ + if(file.isDirectory()){ + this.recursivelyRegisterCachedFiles(tsCache, fileMap, file); + } else if(file.getPath().endsWith(".ts") || file.getPath().endsWith(".js")){ + try { + String relativePath = FileUtils.relativize(file, tsCache); + String normalizedPath = "/" + relativePath; - /** - * Recompiles the scripting engine - */ - private void recompile(Runnable onCompletion){ - Thread recompileThread = new Thread(() -> { - Globals.scriptEngine.executeSynchronously(() -> { - this.initScripts(); - }); - if(onCompletion != null){ - onCompletion.run(); + if(!this.fileChecksumMap.containsKey(normalizedPath)){ + + //read file + String fileContent = Files.readString(file.toPath()); + + //store checksum + try { + this.fileChecksumMap.put(normalizedPath,FileUtils.getChecksum(fileContent)); + } catch (NoSuchAlgorithmException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + //store on script side + LoggerInterface.loggerScripts.DEBUG("Preload: " + normalizedPath); + this.scriptContext.getTopLevelValue("COMPILER").invokeMember("preloadFile", normalizedPath, fileContent); + } + } catch (IOException e) { + LoggerInterface.loggerFileIO.ERROR(e); + } } - }); - recompileThread.setName("Recompile Script Engine"); - recompileThread.start(); - } - - /** - * Initializes a scene script - * @param scenePath The scene's init script path - * @return The id assigned to the scene instance from the script-side - */ - public int initScene(String scenePath){ - //add files to virtual filesystem in script engine - registerFile(scenePath); - //load scene from javascript side - Value sceneLoader = this.engineObject.getMember("sceneLoader"); - Value loadFunc = sceneLoader.getMember("loadScene"); - Value result = loadFunc.execute(scenePath); - return result.asInt(); - } - - - /** - * Calls a function defined in the global scope with the arguments provided - * @param functionName The function name - * @param args The arguments - */ - public Value invokeFunction(String functionName, Object... args){ - LoggerInterface.loggerScripts.DEBUG("Host execute: " + functionName); - Value function = topLevelValue.getMember(functionName); - if(function != null){ - return function.execute(args); - } else { - LoggerInterface.loggerScripts.WARNING("Failed to invoke function " + functionName); } - return null; - } - - /** - * Calls a function on a child of the top level member - * @param memberName The name of the child - * @param functionName The name of the function - * @param args The arguments for the function - * @return The value from the function call - */ - public Value invokeMemberFunction(String memberName, String functionName, Object ... args){ - LoggerInterface.loggerScripts.DEBUG("Host execute: " + functionName); - Value childMember = topLevelValue.getMember(memberName); - Value function = childMember.getMember(functionName); - if(function != null){ - return function.execute(args); - } else { - LoggerInterface.loggerScripts.WARNING("Failed to invoke function " + functionName); - } - return null; - } - - /** - * Invokes a function defined in a file - * @param filePath The file the function is defined in - * @param functionName The function's name - * @param args The args to pass into the function - */ - public void invokeModuleFunction(String filePath, String functionName, Object ... args){ - Value filePathRaw = invokeFunction("FILE_RESOLUTION_getFilePath",filePath); - Value requireCache = topLevelValue.getMember("REQUIRE_CACHE"); - Value module = requireCache.getMember(filePathRaw.asString()); - Value exports = module.getMember("exports"); - Value function = exports.getMember(functionName); - if(function != null && function.canExecute()){ - function.execute(args); - } else { - LoggerInterface.loggerScripts.WARNING("Failed to invoke function " + functionName); - } - } - - /** - * Requires a module into the global space - * @param filePath The filepath of the module - */ - public void requireModule(String filePath){ - invokeFunction("require", filePath); - } - - /** - * Invokes a function on a member of arbitrary depth on the engine object - * @param memberName The member name - * @param functionName The function's name - * @param className The class of the expected return value - * @param args The args to pass to the function - * @return The results of the invocation or null if there was no result - */ - public Value invokeEngineMember(String memberName, String functionName, Object ... args){ - Value member = this.engineObject.getMember(memberName); - if(member == null){ - throw new Error("Member is null!"); - } - Value function = member.getMember(functionName); - if(function == null || !function.canExecute()){ - throw new Error("Function is not executable! " + function); - } - Value executionResult = function.execute(args); - if(executionResult == null){ - return null; - } - return executionResult; - } - - /** - * Executes some code synchronously that requires script engine access - * @param function The function - */ - public void executeSynchronously(Runnable function){ - lock.lock(); - function.run(); - lock.unlock(); - } - - /** - * Defines host members within javascript context - */ - private void defineHostMembers(){ - hostObject = topLevelValue.getMember("HOST_ACCESS"); - //give guest access to static classes - Value classes = engineObject.getMember("classes"); - for(Object[] currentClass : staticClasses){ - classes.putMember((String)currentClass[0], currentClass[1]); - } - //give access to script engine instance - hostObject.putMember("scriptEngine", this); - } - - /** - * Fires a signal on a given scene - * @param signal The signal name - * @param sceneInstanceId The script-side instanceid of the scene - * @param args The arguments to accompany the signal invocation - */ - public void fireSignal(String signal, int sceneInstanceId, Object ... args){ - Value fireSignal = this.hookManager.getMember("fireSignal"); - fireSignal.execute(sceneInstanceId,signal,args); } /** @@ -506,9 +289,9 @@ public class ScriptEngine extends SignalServiceImpl { switch(signal.getType()){ case SCRIPT_RECOMPILE: { if(signal.getData() != null && signal.getData() instanceof Runnable){ - this.recompile((Runnable)signal.getData()); + scriptContext.recompile((Runnable)signal.getData()); } else { - this.recompile(null); + scriptContext.recompile(null); } rVal = true; } break; diff --git a/src/main/java/electrosphere/server/datacell/Realm.java b/src/main/java/electrosphere/server/datacell/Realm.java index 827eb5a9..ec338a89 100644 --- a/src/main/java/electrosphere/server/datacell/Realm.java +++ b/src/main/java/electrosphere/server/datacell/Realm.java @@ -276,11 +276,11 @@ public class Realm { */ public void fireSignal(String signalName, Object ... args){ if(Globals.scriptEngine != null && Globals.scriptEngine.isInitialized()){ - Globals.scriptEngine.executeSynchronously(() -> { + Globals.scriptEngine.getScriptContext().executeSynchronously(() -> { if(this.sceneInstanceId != NO_SCENE_INSTANCE){ - Globals.scriptEngine.fireSignal(signalName, sceneInstanceId, args); + Globals.scriptEngine.getScriptContext().fireSignal(signalName, sceneInstanceId, args); } else { - Globals.scriptEngine.fireSignal(signalName, ScriptEngine.GLOBAL_SCENE, args); + Globals.scriptEngine.getScriptContext().fireSignal(signalName, ScriptEngine.GLOBAL_SCENE, args); } }); } diff --git a/src/main/java/electrosphere/server/terrain/generation/TestGenerationChunkGenerator.java b/src/main/java/electrosphere/server/terrain/generation/TestGenerationChunkGenerator.java index 76834c39..87957b7e 100644 --- a/src/main/java/electrosphere/server/terrain/generation/TestGenerationChunkGenerator.java +++ b/src/main/java/electrosphere/server/terrain/generation/TestGenerationChunkGenerator.java @@ -147,11 +147,11 @@ public class TestGenerationChunkGenerator implements ChunkGenerator { VoxelGenerator voxelGenerator = this.tagVoxelMap.get("hills"); if(this.useJavascript){ - Globals.scriptEngine.executeSynchronously(() -> { + Globals.scriptEngine.getScriptContext().executeSynchronously(() -> { int firstType = -2; boolean homogenous = true; GeneratedVoxel voxel = new GeneratedVoxel(); - Value getVoxelFunc = Globals.scriptEngine.invokeEngineMember("chunkGeneratorManager", "getVoxelFunction", SCRIPT_GEN_TEST_TAG); + Value getVoxelFunc = Globals.scriptEngine.getScriptContext().invokeEngineMember("chunkGeneratorManager", "getVoxelFunction", SCRIPT_GEN_TEST_TAG); for(int x = 0; x < ServerTerrainChunk.CHUNK_DATA_GENERATOR_SIZE; x++){ Globals.profiler.beginAggregateCpuSample("TestGenerationChunkGenerator - Generate slice"); for(int y = 0; y < ServerTerrainChunk.CHUNK_DATA_GENERATOR_SIZE; y++){ diff --git a/src/main/java/electrosphere/util/FileUtils.java b/src/main/java/electrosphere/util/FileUtils.java index dfbba734..c39c62f4 100644 --- a/src/main/java/electrosphere/util/FileUtils.java +++ b/src/main/java/electrosphere/util/FileUtils.java @@ -12,13 +12,18 @@ import electrosphere.util.annotation.AnnotationExclusionStrategy; import java.awt.image.BufferedImage; import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.io.ObjectOutputStream; +import java.io.Serializable; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.util.LinkedList; import java.util.List; import java.util.concurrent.TimeUnit; @@ -421,8 +426,47 @@ public class FileUtils { LoggerInterface.loggerRenderer.ERROR(e); } } + + /** + * Gets a file's path relative to a given directory + * @param file The file + * @param directory The directory + * @return The relative path + */ + public static String relativize(File file, File directory){ + return directory.toURI().relativize(file.toURI()).getPath(); + } + + /** + * Computes the checksum of an object + * @param object The object + * @return The checksum + * @throws IOException Thrown on io errors reading the file + * @throws NoSuchAlgorithmException Thrown if MD5 isn't supported + */ + public static String getChecksum(Serializable object) throws IOException, NoSuchAlgorithmException { + ByteArrayOutputStream baos = null; + ObjectOutputStream oos = null; + try { + baos = new ByteArrayOutputStream(); + oos = new ObjectOutputStream(baos); + oos.writeObject(object); + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] bytes = md.digest(baos.toByteArray()); + StringBuffer builder = new StringBuffer(); + for(byte byteCurr : bytes){ + builder.append(String.format("%02x",byteCurr)); + } + return builder.toString(); + } finally { + oos.close(); + baos.close(); + } + } + + }