major script engine work
All checks were successful
studiorailgun/Renderer/pipeline/head This commit looks good

This commit is contained in:
austin 2024-11-15 15:10:42 -05:00
parent 64a3628316
commit d0235a01d6
22 changed files with 990 additions and 561 deletions

View File

@ -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)

View File

@ -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

View File

@ -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",

View File

@ -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);
}
});
}

View File

@ -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<CreatureTemplate> 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);
})
);

View File

@ -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<VoxelType> 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<VoxelType> 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<VoxelType> 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);
}
});
}
}

View File

@ -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);
}
}
}

View File

@ -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<VoxelType> 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<VoxelType> onSelectType){
scrollable.clearChildren();
VoxelData voxelData = Globals.gameConfigCurrent.getVoxelData();
List<VoxelType> 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

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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;
}});

View File

@ -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;

View File

@ -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();
}
///
///

View File

@ -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

View File

@ -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);
});
}

View File

@ -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);
}
}

View File

@ -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

View File

@ -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);
}
}

View File

@ -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<String,Source> 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<String,String> fileChecksumMap = new HashMap<String,String>();
/**
* 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<String,Source>();
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<Path>(){
@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<String,Source>();
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<WatchEvent<?>> 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;

View File

@ -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);
}
});
}

View File

@ -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++){

View File

@ -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();
}
}
}