major script engine work
All checks were successful
studiorailgun/Renderer/pipeline/head This commit looks good
All checks were successful
studiorailgun/Renderer/pipeline/head This commit looks good
This commit is contained in:
parent
64a3628316
commit
d0235a01d6
@ -60,6 +60,28 @@ let COMPILER = {
|
|||||||
*/
|
*/
|
||||||
currentDirectory : { },
|
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
|
* Registers a file with the compiler
|
||||||
* @param {string} fileName The file's name
|
* @param {string} fileName The file's name
|
||||||
@ -409,10 +431,7 @@ COMPILER.customCompilerHost = {
|
|||||||
writeFile: (fileName, data) => {
|
writeFile: (fileName, data) => {
|
||||||
loggerScripts.INFO("EMIT FILE " + fileName)
|
loggerScripts.INFO("EMIT FILE " + fileName)
|
||||||
//wrap in require logic
|
//wrap in require logic
|
||||||
let finalData =
|
let finalData = COMPILER.getModuleContent(data)
|
||||||
"let exports = { }\n" +
|
|
||||||
data + "\n" +
|
|
||||||
"return exports"
|
|
||||||
|
|
||||||
//create file
|
//create file
|
||||||
COMPILER.createFile(fileName,finalData)
|
COMPILER.createFile(fileName,finalData)
|
||||||
|
|||||||
@ -1042,6 +1042,13 @@ Add concept of interaction definition in common entity type
|
|||||||
Fix bug with entity ray casting selecting player entity
|
Fix bug with entity ray casting selecting player entity
|
||||||
Fix crafting menu yoga appliation logic
|
Fix crafting menu yoga appliation logic
|
||||||
Workbench can open crafting menu
|
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
|
- Recipe definitions
|
||||||
- Reagent items
|
- Reagent items
|
||||||
|
|
||||||
|
|
||||||
Ability to fully reload game engine state without exiting client
|
Ability to fully reload game engine state without exiting client
|
||||||
- Back out to main menu and load a new level without any values persisting
|
- 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
|
- 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 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
|
- Fix typescript load error
|
||||||
- Calculate bounding sphere for meshes by deforming vertices with bone default pose instead of no bone deform
|
- 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 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 light cluster mapping for foliage shader
|
||||||
- Fix foliage placement
|
- Fix foliage placement
|
||||||
@ -1092,8 +1097,8 @@ Bug Fixes
|
|||||||
- Fix block tree preventing initiating an attack
|
- Fix block tree preventing initiating an attack
|
||||||
- Fix return to title menu synchronization bug
|
- Fix return to title menu synchronization bug
|
||||||
- Fix particles not spawning in correct positions
|
- Fix particles not spawning in correct positions
|
||||||
- Fix level creation menu button alignment
|
- Fix flickering when applying yoga signal (may need to rethink arch here)
|
||||||
- Fix spawn palette menu alignment
|
- Fix virtual scrollables not working
|
||||||
|
|
||||||
Startup Performance
|
Startup Performance
|
||||||
- Cache loaded typescript
|
- Cache loaded typescript
|
||||||
@ -1127,16 +1132,6 @@ Code cleanup
|
|||||||
- Rename "ShaderProgram" to "VisualShader"
|
- Rename "ShaderProgram" to "VisualShader"
|
||||||
- Have ComputeShader and VisualShader use same static method for uploading uniforms
|
- 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
|
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
|
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
|
Shader library system
|
||||||
- Abiltiy to include the shader library in individual files (ie implement #include)
|
- 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
|
Another pass at grass
|
||||||
- Multiple foliage models in same cell
|
- Multiple foliage models in same cell
|
||||||
|
|
||||||
|
|||||||
@ -40,6 +40,9 @@ public class ItemActions {
|
|||||||
Vector3d eyePos = new Vector3d(CameraEntityUtils.getCameraEye(Globals.playerCamera));
|
Vector3d eyePos = new Vector3d(CameraEntityUtils.getCameraEye(Globals.playerCamera));
|
||||||
Vector3d centerPos = new Vector3d(CameraEntityUtils.getCameraCenter(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);
|
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
|
//tell the server we want the secondary hand item to START doing something
|
||||||
Globals.clientConnection.queueOutgoingMessage(InventoryMessage.constructclientRequestPerformItemActionMessage(
|
Globals.clientConnection.queueOutgoingMessage(InventoryMessage.constructclientRequestPerformItemActionMessage(
|
||||||
"handRight",
|
"handRight",
|
||||||
@ -77,6 +80,9 @@ public class ItemActions {
|
|||||||
Vector3d eyePos = new Vector3d(CameraEntityUtils.getCameraEye(Globals.playerCamera));
|
Vector3d eyePos = new Vector3d(CameraEntityUtils.getCameraEye(Globals.playerCamera));
|
||||||
Vector3d centerPos = new Vector3d(CameraEntityUtils.getCameraCenter(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);
|
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
|
//tell the server we want the secondary hand item to STOP doing something
|
||||||
Globals.clientConnection.queueOutgoingMessage(InventoryMessage.constructclientRequestPerformItemActionMessage(
|
Globals.clientConnection.queueOutgoingMessage(InventoryMessage.constructclientRequestPerformItemActionMessage(
|
||||||
"handRight",
|
"handRight",
|
||||||
@ -96,6 +102,9 @@ public class ItemActions {
|
|||||||
Vector3d eyePos = new Vector3d(CameraEntityUtils.getCameraEye(Globals.playerCamera));
|
Vector3d eyePos = new Vector3d(CameraEntityUtils.getCameraEye(Globals.playerCamera));
|
||||||
Vector3d centerPos = new Vector3d(CameraEntityUtils.getCameraCenter(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);
|
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
|
//tell the server we want the secondary hand item to STOP doing something
|
||||||
Globals.clientConnection.queueOutgoingMessage(InventoryMessage.constructclientRequestPerformItemActionMessage(
|
Globals.clientConnection.queueOutgoingMessage(InventoryMessage.constructclientRequestPerformItemActionMessage(
|
||||||
"handRight",
|
"handRight",
|
||||||
@ -123,6 +132,9 @@ public class ItemActions {
|
|||||||
Vector3d eyePos = new Vector3d(CameraEntityUtils.getCameraEye(Globals.playerCamera));
|
Vector3d eyePos = new Vector3d(CameraEntityUtils.getCameraEye(Globals.playerCamera));
|
||||||
Vector3d centerPos = new Vector3d(CameraEntityUtils.getCameraCenter(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);
|
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
|
//tell the server we want the secondary hand item to START doing something
|
||||||
Globals.clientConnection.queueOutgoingMessage(InventoryMessage.constructclientRequestPerformItemActionMessage(
|
Globals.clientConnection.queueOutgoingMessage(InventoryMessage.constructclientRequestPerformItemActionMessage(
|
||||||
"handRight",
|
"handRight",
|
||||||
@ -152,6 +164,9 @@ public class ItemActions {
|
|||||||
Vector3d eyePos = new Vector3d(CameraEntityUtils.getCameraEye(Globals.playerCamera));
|
Vector3d eyePos = new Vector3d(CameraEntityUtils.getCameraEye(Globals.playerCamera));
|
||||||
Vector3d centerPos = new Vector3d(CameraEntityUtils.getCameraCenter(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);
|
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
|
//tell the server we want the secondary hand item to STOP doing something
|
||||||
Globals.clientConnection.queueOutgoingMessage(InventoryMessage.constructclientRequestPerformItemActionMessage(
|
Globals.clientConnection.queueOutgoingMessage(InventoryMessage.constructclientRequestPerformItemActionMessage(
|
||||||
"handRight",
|
"handRight",
|
||||||
|
|||||||
@ -14,9 +14,9 @@ public class ClientScriptUtils {
|
|||||||
* @param args The arguments provided alongside the signal
|
* @param args The arguments provided alongside the signal
|
||||||
*/
|
*/
|
||||||
public static void fireSignal(String signalName, Object ... args){
|
public static void fireSignal(String signalName, Object ... args){
|
||||||
Globals.scriptEngine.executeSynchronously(() -> {
|
Globals.scriptEngine.getScriptContext().executeSynchronously(() -> {
|
||||||
if(Globals.scriptEngine != null && Globals.scriptEngine.isInitialized()){
|
if(Globals.scriptEngine != null && Globals.scriptEngine.isInitialized()){
|
||||||
Globals.scriptEngine.fireSignal(signalName, ScriptEngine.GLOBAL_SCENE, args);
|
Globals.scriptEngine.getScriptContext().fireSignal(signalName, ScriptEngine.GLOBAL_SCENE, args);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package electrosphere.client.ui.components;
|
package electrosphere.client.ui.components;
|
||||||
|
|
||||||
|
import java.util.function.Consumer;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.joml.Vector3f;
|
import org.joml.Vector3f;
|
||||||
@ -45,7 +46,7 @@ public class CharacterCustomizer {
|
|||||||
* @param race The race of the character
|
* @param race The race of the character
|
||||||
* @return The panel component
|
* @return The panel component
|
||||||
*/
|
*/
|
||||||
public static Element createCharacterCustomizerPanel(String race){
|
public static Element createCharacterCustomizerPanel(String race, Consumer<CreatureTemplate> onConfirm){
|
||||||
//figure out race data
|
//figure out race data
|
||||||
CreatureData selectedRaceType = Globals.gameConfigCurrent.getCreatureTypeLoader().getType(race);
|
CreatureData selectedRaceType = Globals.gameConfigCurrent.getCreatureTypeLoader().getType(race);
|
||||||
|
|
||||||
@ -149,6 +150,7 @@ public class CharacterCustomizer {
|
|||||||
),
|
),
|
||||||
Button.createButton("Create", () -> {
|
Button.createButton("Create", () -> {
|
||||||
Globals.clientConnection.queueOutgoingMessage(CharacterMessage.constructRequestCreateCharacterMessage(Utilities.stringify(template)));
|
Globals.clientConnection.queueOutgoingMessage(CharacterMessage.constructRequestCreateCharacterMessage(Utilities.stringify(template)));
|
||||||
|
onConfirm.accept(template);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
24
src/main/java/electrosphere/client/ui/menu/YogaUtils.java
Normal file
24
src/main/java/electrosphere/client/ui/menu/YogaUtils.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,32 +1,19 @@
|
|||||||
package electrosphere.client.ui.menu.ingame;
|
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.SpawnSelectionPanel;
|
||||||
|
import electrosphere.client.ui.components.VoxelSelectionPanel;
|
||||||
import electrosphere.client.ui.menu.WindowStrings;
|
import electrosphere.client.ui.menu.WindowStrings;
|
||||||
import electrosphere.client.ui.menu.WindowUtils;
|
import electrosphere.client.ui.menu.WindowUtils;
|
||||||
import electrosphere.controls.ControlHandler.ControlsState;
|
import electrosphere.controls.ControlHandler.ControlsState;
|
||||||
import electrosphere.engine.Globals;
|
import electrosphere.engine.Globals;
|
||||||
import electrosphere.engine.signal.Signal.SignalType;
|
import electrosphere.engine.signal.Signal.SignalType;
|
||||||
import electrosphere.game.data.common.CommonEntityType;
|
import electrosphere.game.data.common.CommonEntityType;
|
||||||
import electrosphere.game.data.voxel.VoxelData;
|
|
||||||
import electrosphere.game.data.voxel.VoxelType;
|
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.elements.Window;
|
||||||
import electrosphere.renderer.ui.elementtypes.ClickableElement.ClickEventCallback;
|
|
||||||
import electrosphere.renderer.ui.elementtypes.ContainerElement.YogaAlignment;
|
import electrosphere.renderer.ui.elementtypes.ContainerElement.YogaAlignment;
|
||||||
import electrosphere.renderer.ui.elementtypes.ContainerElement.YogaFlexDirection;
|
import electrosphere.renderer.ui.elementtypes.ContainerElement.YogaFlexDirection;
|
||||||
import electrosphere.renderer.ui.elementtypes.ContainerElement.YogaJustification;
|
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.elementtypes.NavigableElement.NavigationEventCallback;
|
||||||
import electrosphere.renderer.ui.events.ClickEvent;
|
|
||||||
import electrosphere.renderer.ui.events.KeyboardEvent;
|
|
||||||
import electrosphere.renderer.ui.events.NavigationEvent;
|
import electrosphere.renderer.ui.events.NavigationEvent;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -34,23 +21,8 @@ import electrosphere.renderer.ui.events.NavigationEvent;
|
|||||||
*/
|
*/
|
||||||
public class MenuGeneratorsTerrainEditing {
|
public class MenuGeneratorsTerrainEditing {
|
||||||
|
|
||||||
static Window terrainEditingSidePanelWindow;
|
|
||||||
static Window entitySelectionWindow;
|
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
|
//width of the side panel
|
||||||
static final int WINDOW_WIDTH = 550;
|
static final int WINDOW_WIDTH = 550;
|
||||||
static final int WINDOW_HEIGHT = 550;
|
static final int WINDOW_HEIGHT = 550;
|
||||||
@ -61,7 +33,7 @@ public class MenuGeneratorsTerrainEditing {
|
|||||||
*/
|
*/
|
||||||
public static Window createVoxelTypeSelectionPanel(){
|
public static Window createVoxelTypeSelectionPanel(){
|
||||||
//setup window
|
//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.setParentAlignContent(YogaAlignment.Center);
|
||||||
terrainEditingSidePanelWindow.setParentJustifyContent(YogaJustification.Center);
|
terrainEditingSidePanelWindow.setParentJustifyContent(YogaJustification.Center);
|
||||||
terrainEditingSidePanelWindow.setParentAlignItem(YogaAlignment.Center);
|
terrainEditingSidePanelWindow.setParentAlignItem(YogaAlignment.Center);
|
||||||
@ -79,120 +51,15 @@ public class MenuGeneratorsTerrainEditing {
|
|||||||
}});
|
}});
|
||||||
|
|
||||||
//attach scrollable after search input for organzation purposes
|
//attach scrollable after search input for organzation purposes
|
||||||
terrainEditingSidePanelWindow.addChild(createVoxelTypeSelectionPanel((VoxelType type) -> {
|
terrainEditingSidePanelWindow.addChild(VoxelSelectionPanel.createVoxelTypeSelectionPanel((VoxelType type) -> {
|
||||||
Globals.clientSelectedVoxelType = type;
|
Globals.clientSelectedVoxelType = type;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
Globals.signalSystem.post(SignalType.YOGA_APPLY,terrainEditingSidePanelWindow);
|
Globals.signalSystem.post(SignalType.YOGA_APPLY,terrainEditingSidePanelWindow);
|
||||||
|
|
||||||
return 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
|
* Creates the entity type selection window
|
||||||
* @return
|
* @return
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -3,8 +3,8 @@ package electrosphere.client.ui.menu.mainmenu;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import electrosphere.client.ui.components.InputMacros;
|
import electrosphere.client.ui.components.InputMacros;
|
||||||
|
import electrosphere.client.ui.components.VoxelSelectionPanel;
|
||||||
import electrosphere.client.ui.menu.WindowUtils;
|
import electrosphere.client.ui.menu.WindowUtils;
|
||||||
import electrosphere.client.ui.menu.ingame.MenuGeneratorsTerrainEditing;
|
|
||||||
import electrosphere.engine.Globals;
|
import electrosphere.engine.Globals;
|
||||||
import electrosphere.engine.assetmanager.AssetDataStrings;
|
import electrosphere.engine.assetmanager.AssetDataStrings;
|
||||||
import electrosphere.engine.loadingthreads.LoadingThread;
|
import electrosphere.engine.loadingthreads.LoadingThread;
|
||||||
@ -194,7 +194,7 @@ public class MenuGeneratorsLevelEditor {
|
|||||||
}, DEFAULT_GRID_SIZE / (float)GriddedDataCellManager.MAX_GRID_SIZE)
|
}, DEFAULT_GRID_SIZE / (float)GriddedDataCellManager.MAX_GRID_SIZE)
|
||||||
);
|
);
|
||||||
sceneFile.getRealmDescriptor().setGriddedRealmSize(DEFAULT_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());
|
sceneFile.getRealmDescriptor().setBaseVoxel(type.getId());
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@ -205,11 +205,13 @@ public class MenuGeneratorsLevelEditor {
|
|||||||
//
|
//
|
||||||
//Create level button
|
//Create level button
|
||||||
//
|
//
|
||||||
rVal.addChild(Button.createButton("Create Level", () -> {
|
Button createButton = Button.createButton("Create Level", () -> {
|
||||||
//launch level editor
|
//launch level editor
|
||||||
LoadingThread loadingThread = new LoadingThread(LoadingThreadType.LEVEL_EDITOR, inFlightLevel);
|
LoadingThread loadingThread = new LoadingThread(LoadingThreadType.LEVEL_EDITOR, inFlightLevel);
|
||||||
Globals.threadManager.start(loadingThread);
|
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;
|
return rVal;
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import electrosphere.client.ui.components.CharacterCustomizer;
|
|||||||
import electrosphere.client.ui.menu.MenuGenerators;
|
import electrosphere.client.ui.menu.MenuGenerators;
|
||||||
import electrosphere.client.ui.menu.WindowUtils;
|
import electrosphere.client.ui.menu.WindowUtils;
|
||||||
import electrosphere.engine.Globals;
|
import electrosphere.engine.Globals;
|
||||||
|
import electrosphere.entity.types.creature.CreatureTemplate;
|
||||||
import electrosphere.renderer.ui.elements.Button;
|
import electrosphere.renderer.ui.elements.Button;
|
||||||
import electrosphere.renderer.ui.elements.FormElement;
|
import electrosphere.renderer.ui.elements.FormElement;
|
||||||
import electrosphere.renderer.ui.elements.Label;
|
import electrosphere.renderer.ui.elements.Label;
|
||||||
@ -52,7 +53,7 @@ public class MenuGeneratorsMultiplayer {
|
|||||||
createButton.addChild(createLabel);
|
createButton.addChild(createLabel);
|
||||||
rVal.addChild(createButton);
|
rVal.addChild(createButton);
|
||||||
createButton.setOnClick(new ClickableElement.ClickEventCallback(){public boolean execute(ClickEvent event){
|
createButton.setOnClick(new ClickableElement.ClickEventCallback(){public boolean execute(ClickEvent event){
|
||||||
WindowUtils.replaceMainMenuContents(CharacterCustomizer.createCharacterCustomizerPanel(selectedRace));
|
WindowUtils.replaceMainMenuContents(CharacterCustomizer.createCharacterCustomizerPanel(selectedRace, (CreatureTemplate template) -> {}));
|
||||||
return false;
|
return false;
|
||||||
}});
|
}});
|
||||||
|
|
||||||
|
|||||||
@ -13,8 +13,8 @@ import electrosphere.client.ui.components.EquipmentInventoryPanel;
|
|||||||
import electrosphere.client.ui.components.InputMacros;
|
import electrosphere.client.ui.components.InputMacros;
|
||||||
import electrosphere.client.ui.components.NaturalInventoryPanel;
|
import electrosphere.client.ui.components.NaturalInventoryPanel;
|
||||||
import electrosphere.client.ui.components.SpawnSelectionPanel;
|
import electrosphere.client.ui.components.SpawnSelectionPanel;
|
||||||
|
import electrosphere.client.ui.components.VoxelSelectionPanel;
|
||||||
import electrosphere.client.ui.menu.WindowUtils;
|
import electrosphere.client.ui.menu.WindowUtils;
|
||||||
import electrosphere.client.ui.menu.ingame.MenuGeneratorsTerrainEditing;
|
|
||||||
import electrosphere.engine.Globals;
|
import electrosphere.engine.Globals;
|
||||||
import electrosphere.engine.assetmanager.AssetDataStrings;
|
import electrosphere.engine.assetmanager.AssetDataStrings;
|
||||||
import electrosphere.entity.Entity;
|
import electrosphere.entity.Entity;
|
||||||
@ -22,6 +22,7 @@ import electrosphere.entity.EntityCreationUtils;
|
|||||||
import electrosphere.entity.state.inventory.InventoryUtils;
|
import electrosphere.entity.state.inventory.InventoryUtils;
|
||||||
import electrosphere.entity.state.inventory.RelationalInventoryState;
|
import electrosphere.entity.state.inventory.RelationalInventoryState;
|
||||||
import electrosphere.entity.state.inventory.UnrelationalInventoryState;
|
import electrosphere.entity.state.inventory.UnrelationalInventoryState;
|
||||||
|
import electrosphere.entity.types.creature.CreatureTemplate;
|
||||||
import electrosphere.game.data.common.CommonEntityType;
|
import electrosphere.game.data.common.CommonEntityType;
|
||||||
import electrosphere.game.data.creature.type.equip.EquipPoint;
|
import electrosphere.game.data.creature.type.equip.EquipPoint;
|
||||||
import electrosphere.game.data.voxel.VoxelType;
|
import electrosphere.game.data.voxel.VoxelType;
|
||||||
@ -123,7 +124,7 @@ public class MenuGeneratorsUITesting {
|
|||||||
formEl.addChild(Button.createButton("test", () -> {}));
|
formEl.addChild(Button.createButton("test", () -> {}));
|
||||||
} break;
|
} break;
|
||||||
case "CharacterCustomizer": {
|
case "CharacterCustomizer": {
|
||||||
formEl.addChild(CharacterCustomizer.createCharacterCustomizerPanel("human"));
|
formEl.addChild(CharacterCustomizer.createCharacterCustomizerPanel("human", (CreatureTemplate template) ->{}));
|
||||||
} break;
|
} break;
|
||||||
case "NaturalInventoryPanel": {
|
case "NaturalInventoryPanel": {
|
||||||
Entity ent = EntityCreationUtils.TEST_createEntity();
|
Entity ent = EntityCreationUtils.TEST_createEntity();
|
||||||
@ -144,7 +145,7 @@ public class MenuGeneratorsUITesting {
|
|||||||
formEl.addChild(EquipmentInventoryPanel.createEquipmentInventoryPanel(ent));
|
formEl.addChild(EquipmentInventoryPanel.createEquipmentInventoryPanel(ent));
|
||||||
} break;
|
} break;
|
||||||
case "VoxelPicker": {
|
case "VoxelPicker": {
|
||||||
formEl.addChild(MenuGeneratorsTerrainEditing.createVoxelTypeSelectionPanel((VoxelType voxelType) -> {
|
formEl.addChild(VoxelSelectionPanel.createVoxelTypeSelectionPanel((VoxelType voxelType) -> {
|
||||||
System.out.println(voxelType.getName());
|
System.out.println(voxelType.getName());
|
||||||
}));
|
}));
|
||||||
} break;
|
} break;
|
||||||
|
|||||||
@ -300,6 +300,13 @@ public class Main {
|
|||||||
///
|
///
|
||||||
Globals.scriptEngine.handleAllSignals();
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
///
|
///
|
||||||
///
|
///
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import electrosphere.client.terrain.cells.ClientDrawCellManager;
|
|||||||
import electrosphere.client.ui.menu.MenuGenerators;
|
import electrosphere.client.ui.menu.MenuGenerators;
|
||||||
import electrosphere.client.ui.menu.WindowStrings;
|
import electrosphere.client.ui.menu.WindowStrings;
|
||||||
import electrosphere.client.ui.menu.WindowUtils;
|
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.controls.ControlHandler;
|
||||||
import electrosphere.engine.Globals;
|
import electrosphere.engine.Globals;
|
||||||
import electrosphere.engine.assetmanager.AssetDataStrings;
|
import electrosphere.engine.assetmanager.AssetDataStrings;
|
||||||
@ -45,6 +45,10 @@ public class ClientLoading {
|
|||||||
static final int DRAW_CELL_EXPECTED_MINIMUM_FRAMES_TO_INIT = 10;
|
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){
|
protected static void loadCharacterServer(Object[] params){
|
||||||
WindowUtils.recursiveSetVisible(Globals.elementService.getWindow(WindowStrings.WINDOW_MENU_MAIN), false);
|
WindowUtils.recursiveSetVisible(Globals.elementService.getWindow(WindowStrings.WINDOW_MENU_MAIN), false);
|
||||||
WindowUtils.replaceMainMenuContents(MenuGenerators.createEmptyMainMenu());
|
WindowUtils.replaceMainMenuContents(MenuGenerators.createEmptyMainMenu());
|
||||||
@ -63,7 +67,7 @@ public class ClientLoading {
|
|||||||
//once we have them, bring up the character creation interface
|
//once we have them, bring up the character creation interface
|
||||||
//init character creation window
|
//init character creation window
|
||||||
//eventually should replace with at ui to select an already created character or create a new one
|
//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
|
//make loading dialog disappear
|
||||||
WindowUtils.recursiveSetVisible(Globals.elementService.getWindow(WindowStrings.WINDOW_LOADING), false);
|
WindowUtils.recursiveSetVisible(Globals.elementService.getWindow(WindowStrings.WINDOW_LOADING), false);
|
||||||
//make character creation window visible
|
//make character creation window visible
|
||||||
|
|||||||
@ -125,8 +125,8 @@ public class SceneLoader {
|
|||||||
//load scripts
|
//load scripts
|
||||||
if(!isLevelEditor && file.getInitScriptPath() != null){
|
if(!isLevelEditor && file.getInitScriptPath() != null){
|
||||||
Realm finalRealm = realm;
|
Realm finalRealm = realm;
|
||||||
Globals.scriptEngine.executeSynchronously(() -> {
|
Globals.scriptEngine.getScriptContext().executeSynchronously(() -> {
|
||||||
int sceneInstanceId = Globals.scriptEngine.initScene(file.getInitScriptPath());
|
int sceneInstanceId = Globals.scriptEngine.getScriptContext().initScene(file.getInitScriptPath());
|
||||||
finalRealm.setSceneInstanceId(sceneInstanceId);
|
finalRealm.setSceneInstanceId(sceneInstanceId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,9 +22,17 @@ import electrosphere.renderer.ui.events.FocusEvent;
|
|||||||
import electrosphere.renderer.ui.events.HoverEvent;
|
import electrosphere.renderer.ui.events.HoverEvent;
|
||||||
import electrosphere.renderer.ui.events.MouseEvent;
|
import electrosphere.renderer.ui.events.MouseEvent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A button element
|
||||||
|
*/
|
||||||
public class Button extends StandardContainerElement implements DrawableElement, FocusableElement, ClickableElement, HoverableElement {
|
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 boxPosition = new Vector3f();
|
||||||
Vector3f boxDimensions = new Vector3f();
|
Vector3f boxDimensions = new Vector3f();
|
||||||
@ -302,5 +310,13 @@ public class Button extends StandardContainerElement implements DrawableElement,
|
|||||||
this.audioPathOnClick = audioPath;
|
this.audioPathOnClick = audioPath;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the background color of this element
|
||||||
|
* @param color The color
|
||||||
|
*/
|
||||||
|
public void setColor(Vector3f color){
|
||||||
|
this.color.set(color);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,18 +5,12 @@ import electrosphere.renderer.model.Material;
|
|||||||
import electrosphere.renderer.texture.Texture;
|
import electrosphere.renderer.texture.Texture;
|
||||||
|
|
||||||
import java.awt.image.BufferedImage;
|
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.RenderingHints;
|
||||||
import java.awt.FontMetrics;
|
import java.awt.FontMetrics;
|
||||||
import java.awt.Graphics2D;
|
import java.awt.Graphics2D;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import javax.imageio.ImageIO;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utilities for loading fonts
|
* Utilities for loading fonts
|
||||||
@ -140,15 +134,15 @@ public class FontUtils {
|
|||||||
// AffineTransformOp operation = new AffineTransformOp(transform, AffineTransformOp.TYPE_NEAREST_NEIGHBOR);
|
// AffineTransformOp operation = new AffineTransformOp(transform, AffineTransformOp.TYPE_NEAREST_NEIGHBOR);
|
||||||
// image = operation.filter(image, null);
|
// image = operation.filter(image, null);
|
||||||
|
|
||||||
try {
|
// try {
|
||||||
File imgFile = new File("./.testcache/testimg.png");
|
// File imgFile = new File("./.testcache/testimg.png");
|
||||||
imgFile.delete();
|
// imgFile.delete();
|
||||||
imgFile.getParentFile().mkdirs();
|
// imgFile.getParentFile().mkdirs();
|
||||||
ImageIO.write(image, "png", Files.newOutputStream(imgFile.toPath()));
|
// ImageIO.write(image, "png", Files.newOutputStream(imgFile.toPath()));
|
||||||
} catch (IOException e) {
|
// } catch (IOException e) {
|
||||||
// TODO Auto-generated catch block
|
// // TODO Auto-generated catch block
|
||||||
e.printStackTrace();
|
// e.printStackTrace();
|
||||||
}
|
// }
|
||||||
|
|
||||||
|
|
||||||
//create material with new font image
|
//create material with new font image
|
||||||
|
|||||||
409
src/main/java/electrosphere/script/ScriptContext.java
Normal file
409
src/main/java/electrosphere/script/ScriptContext.java
Normal 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -2,16 +2,23 @@ package electrosphere.script;
|
|||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
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.HashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
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;
|
||||||
import org.graalvm.polyglot.Source.Builder;
|
|
||||||
import org.graalvm.polyglot.Value;
|
import org.graalvm.polyglot.Value;
|
||||||
|
|
||||||
import electrosphere.client.script.ScriptClientVoxelUtils;
|
import electrosphere.client.script.ScriptClientVoxelUtils;
|
||||||
@ -29,55 +36,60 @@ import electrosphere.util.FileUtils;
|
|||||||
import electrosphere.util.math.SpatialMathUtils;
|
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 {
|
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
|
* The directory with all script source files
|
||||||
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
|
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
|
* The id for firing signals globally
|
||||||
*/
|
*/
|
||||||
public static final int GLOBAL_SCENE = -1;
|
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
|
//the map of script filepaths to parsed, in-memory scripts
|
||||||
Map<String,Source> sourceMap;
|
Map<String,Source> sourceMap;
|
||||||
|
|
||||||
//the javascript object that stores values
|
/**
|
||||||
Value topLevelValue;
|
* Stores all loaded files' md5 checksums
|
||||||
|
*/
|
||||||
//the object that contains all host values accessible to javascript land
|
Map<String,String> fileChecksumMap = new HashMap<String,String>();
|
||||||
Value hostObject;
|
|
||||||
|
|
||||||
//the engine object
|
|
||||||
Value engineObject;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
* Tracks the initialization status of the script engine
|
||||||
*/
|
*/
|
||||||
boolean initialized = false;
|
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
|
//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
|
//polyfills
|
||||||
"Scripts/compiler/require_polyfill.js",
|
"Scripts/compiler/require_polyfill.js",
|
||||||
|
|
||||||
@ -93,13 +105,13 @@ public class ScriptEngine extends SignalServiceImpl {
|
|||||||
/**
|
/**
|
||||||
* List of files that are ignored when registering new files
|
* 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",
|
"/Scripts/compiler/host_access.ts",
|
||||||
};
|
};
|
||||||
|
|
||||||
//The classes that will be provided to the scripting engine
|
//The classes that will be provided to the scripting engine
|
||||||
//https://stackoverflow.com/a/65942034
|
//https://stackoverflow.com/a/65942034
|
||||||
static final Object[][] staticClasses = new Object[][]{
|
public static final Object[][] staticClasses = new Object[][]{
|
||||||
{"mathUtils",SpatialMathUtils.class},
|
{"mathUtils",SpatialMathUtils.class},
|
||||||
{"simulation",Main.class},
|
{"simulation",Main.class},
|
||||||
{"tutorialUtils",TutorialMenus.class},
|
{"tutorialUtils",TutorialMenus.class},
|
||||||
@ -111,7 +123,7 @@ public class ScriptEngine extends SignalServiceImpl {
|
|||||||
};
|
};
|
||||||
|
|
||||||
//singletons from the host that are provided to the javascript context
|
//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},
|
{"timekeeper",Globals.timekeeper},
|
||||||
{"currentPlayer",Globals.clientPlayer},
|
{"currentPlayer",Globals.clientPlayer},
|
||||||
{"loggerScripts",LoggerInterface.loggerScripts},
|
{"loggerScripts",LoggerInterface.loggerScripts},
|
||||||
@ -123,6 +135,29 @@ public class ScriptEngine extends SignalServiceImpl {
|
|||||||
*/
|
*/
|
||||||
public ScriptEngine(){
|
public ScriptEngine(){
|
||||||
super("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(){
|
public void initScripts(){
|
||||||
//init datastructures
|
//init datastructures
|
||||||
sourceMap = new HashMap<String,Source>();
|
|
||||||
initialized = false;
|
initialized = false;
|
||||||
|
|
||||||
//create engine with flag to disable warning
|
//init script context
|
||||||
Engine engine = Engine.newBuilder()
|
scriptContext.init(this);
|
||||||
.option("engine.WarnInterpreterOnly", "false")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
//Create the rules for guest accessing the host environment
|
//read files from cache
|
||||||
HostAccess accessRules = HostAccess.newBuilder(HostAccess.EXPLICIT)
|
boolean readCache = this.initCache();
|
||||||
.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");
|
|
||||||
|
|
||||||
//compile
|
//compile
|
||||||
compile();
|
if(!readCache){
|
||||||
|
scriptContext.compile();
|
||||||
|
}
|
||||||
|
|
||||||
//run script for engine init
|
//post init logic
|
||||||
requireModule("/Scripts/engine/engine-init.ts");
|
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;
|
initialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores a variable at the top level of the js bindings
|
* Scans the scripts directory for updates
|
||||||
* @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){
|
public void scanScriptDir(){
|
||||||
topLevelValue.putMember(valueName, value);
|
WatchKey key = null;
|
||||||
}
|
while((key = watchService.poll()) != null){
|
||||||
|
List<WatchEvent<?>> events = key.pollEvents();
|
||||||
/**
|
for(WatchEvent<?> event : events){
|
||||||
* Gets a top level value from the script engine
|
if(event.kind() == StandardWatchEventKinds.ENTRY_MODIFY){
|
||||||
* @param valueName The name of the variable
|
if(event.context() instanceof Path){
|
||||||
* @return The value of the variable
|
// Path filePath = (Path)event.context();
|
||||||
*/
|
// System.out.println(filePath);
|
||||||
public Value getTopLevelValue(String valueName){
|
}
|
||||||
return topLevelValue.getMember(valueName);
|
} 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");
|
||||||
* 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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
key.reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads a script from disk
|
* Gets the script context of the engine
|
||||||
* @param path The path to the script file
|
* @return The script context
|
||||||
*/
|
*/
|
||||||
void loadDependency(String path){
|
public ScriptContext getScriptContext(){
|
||||||
String content;
|
return this.scriptContext;
|
||||||
Source source = null;
|
}
|
||||||
try {
|
|
||||||
content = FileUtils.getAssetFileAsString(path);
|
/**
|
||||||
source = Source.create("js",content);
|
* Makes sure the cache folder exists
|
||||||
sourceMap.put(path,source);
|
* @return true if files were read from cache, false otherwise
|
||||||
context.eval(source);
|
*/
|
||||||
} catch (IOException e) {
|
private boolean initCache(){
|
||||||
LoggerInterface.loggerScripts.ERROR("FAILED TO LOAD SCRIPT", e);
|
File tsCache = new File(ScriptEngine.TS_SOURCE_CACHE_DIR);
|
||||||
} catch (PolyglotException e){
|
if(!tsCache.exists()){
|
||||||
if(source != null){
|
try {
|
||||||
LoggerInterface.loggerScripts.WARNING("Source language: " + source.getLanguage());
|
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;
|
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(){
|
private void recursivelyRegisterCachedFiles(File tsCache, Value fileMap, File currentDirectory){
|
||||||
invokeMemberFunction("COMPILER", "run");
|
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;
|
||||||
|
|
||||||
/**
|
if(!this.fileChecksumMap.containsKey(normalizedPath)){
|
||||||
* Recompiles the scripting engine
|
|
||||||
*/
|
//read file
|
||||||
private void recompile(Runnable onCompletion){
|
String fileContent = Files.readString(file.toPath());
|
||||||
Thread recompileThread = new Thread(() -> {
|
|
||||||
Globals.scriptEngine.executeSynchronously(() -> {
|
//store checksum
|
||||||
this.initScripts();
|
try {
|
||||||
});
|
this.fileChecksumMap.put(normalizedPath,FileUtils.getChecksum(fileContent));
|
||||||
if(onCompletion != null){
|
} catch (NoSuchAlgorithmException e) {
|
||||||
onCompletion.run();
|
// 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()){
|
switch(signal.getType()){
|
||||||
case SCRIPT_RECOMPILE: {
|
case SCRIPT_RECOMPILE: {
|
||||||
if(signal.getData() != null && signal.getData() instanceof Runnable){
|
if(signal.getData() != null && signal.getData() instanceof Runnable){
|
||||||
this.recompile((Runnable)signal.getData());
|
scriptContext.recompile((Runnable)signal.getData());
|
||||||
} else {
|
} else {
|
||||||
this.recompile(null);
|
scriptContext.recompile(null);
|
||||||
}
|
}
|
||||||
rVal = true;
|
rVal = true;
|
||||||
} break;
|
} break;
|
||||||
|
|||||||
@ -276,11 +276,11 @@ public class Realm {
|
|||||||
*/
|
*/
|
||||||
public void fireSignal(String signalName, Object ... args){
|
public void fireSignal(String signalName, Object ... args){
|
||||||
if(Globals.scriptEngine != null && Globals.scriptEngine.isInitialized()){
|
if(Globals.scriptEngine != null && Globals.scriptEngine.isInitialized()){
|
||||||
Globals.scriptEngine.executeSynchronously(() -> {
|
Globals.scriptEngine.getScriptContext().executeSynchronously(() -> {
|
||||||
if(this.sceneInstanceId != NO_SCENE_INSTANCE){
|
if(this.sceneInstanceId != NO_SCENE_INSTANCE){
|
||||||
Globals.scriptEngine.fireSignal(signalName, sceneInstanceId, args);
|
Globals.scriptEngine.getScriptContext().fireSignal(signalName, sceneInstanceId, args);
|
||||||
} else {
|
} else {
|
||||||
Globals.scriptEngine.fireSignal(signalName, ScriptEngine.GLOBAL_SCENE, args);
|
Globals.scriptEngine.getScriptContext().fireSignal(signalName, ScriptEngine.GLOBAL_SCENE, args);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -147,11 +147,11 @@ public class TestGenerationChunkGenerator implements ChunkGenerator {
|
|||||||
VoxelGenerator voxelGenerator = this.tagVoxelMap.get("hills");
|
VoxelGenerator voxelGenerator = this.tagVoxelMap.get("hills");
|
||||||
|
|
||||||
if(this.useJavascript){
|
if(this.useJavascript){
|
||||||
Globals.scriptEngine.executeSynchronously(() -> {
|
Globals.scriptEngine.getScriptContext().executeSynchronously(() -> {
|
||||||
int firstType = -2;
|
int firstType = -2;
|
||||||
boolean homogenous = true;
|
boolean homogenous = true;
|
||||||
GeneratedVoxel voxel = new GeneratedVoxel();
|
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++){
|
for(int x = 0; x < ServerTerrainChunk.CHUNK_DATA_GENERATOR_SIZE; x++){
|
||||||
Globals.profiler.beginAggregateCpuSample("TestGenerationChunkGenerator - Generate slice");
|
Globals.profiler.beginAggregateCpuSample("TestGenerationChunkGenerator - Generate slice");
|
||||||
for(int y = 0; y < ServerTerrainChunk.CHUNK_DATA_GENERATOR_SIZE; y++){
|
for(int y = 0; y < ServerTerrainChunk.CHUNK_DATA_GENERATOR_SIZE; y++){
|
||||||
|
|||||||
@ -12,13 +12,18 @@ import electrosphere.util.annotation.AnnotationExclusionStrategy;
|
|||||||
|
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.ObjectOutputStream;
|
||||||
|
import java.io.Serializable;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
@ -421,8 +426,47 @@ public class FileUtils {
|
|||||||
LoggerInterface.loggerRenderer.ERROR(e);
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user