From 50b95d1a9c588d69635b66b21dae307e85c18aac Mon Sep 17 00:00:00 2001 From: simon987 Date: Sat, 1 Aug 2020 20:27:39 -0400 Subject: [PATCH] Trap flag / Debugger first draft #232 --- .../mar/cubot/event/UserCreationListener.java | 2 +- .../net/simon987/mar/npc/ExecuteCpuTask.java | 7 +- .../net/simon987/mar/server/GameServer.java | 38 +++- .../mar/server/assembly/Assembler.java | 66 +++++-- .../mar/server/assembly/AssemblyResult.java | 8 + .../net/simon987/mar/server/assembly/CPU.java | 93 ++++++---- .../simon987/mar/server/assembly/Memory.java | 27 +++ .../simon987/mar/server/assembly/Operand.java | 32 ++++ .../mar/server/assembly/RegisterSet.java | 13 +- .../simon987/mar/server/assembly/Status.java | 12 +- .../simon987/mar/server/assembly/Util.java | 8 +- .../net/simon987/mar/server/user/User.java | 57 ++++++ .../server/websocket/CodeUploadHandler.java | 7 +- .../server/websocket/DebugStepHandler.java | 56 ++++++ .../websocket/DisassemblyRequestHandler.java | 28 +++ .../server/websocket/OnlineUserManager.java | 16 ++ .../mar/server/websocket/SocketServer.java | 17 ++ .../server/websocket/StateRequestHandler.java | 45 +++++ src/main/resources/config.properties | 4 +- src/main/resources/static/css/mar.css | 117 ++++++++++++ src/main/resources/static/js/ace/mode-mar.js | 4 +- src/main/resources/static/js/editor.js | 35 ++-- src/main/resources/templates/header.vm | 10 + src/main/resources/templates/play.vm | 171 ++++++++++-------- src/main/typescript/Console.ts | 2 +- src/main/typescript/GameClient.ts | 121 +++++++++++++ src/main/typescript/MarGame.ts | 18 ++ src/main/typescript/mar.ts | 8 + 28 files changed, 851 insertions(+), 171 deletions(-) create mode 100644 src/main/java/net/simon987/mar/server/websocket/DebugStepHandler.java create mode 100644 src/main/java/net/simon987/mar/server/websocket/DisassemblyRequestHandler.java create mode 100644 src/main/java/net/simon987/mar/server/websocket/StateRequestHandler.java diff --git a/src/main/java/net/simon987/mar/cubot/event/UserCreationListener.java b/src/main/java/net/simon987/mar/cubot/event/UserCreationListener.java index 3322a04..5fdecb2 100644 --- a/src/main/java/net/simon987/mar/cubot/event/UserCreationListener.java +++ b/src/main/java/net/simon987/mar/cubot/event/UserCreationListener.java @@ -62,7 +62,7 @@ public class UserCreationListener implements GameEventListener { user.setUserCode(config.getString("new_user_code")); GameEvent initEvent = new CpuInitialisationEvent(cpu, cubot); - GameServer.INSTANCE.getEventDispatcher().dispatch(event); + GameServer.INSTANCE.getEventDispatcher().dispatch(initEvent); if (initEvent.isCancelled()) { throw new CancelledException(); } diff --git a/src/main/java/net/simon987/mar/npc/ExecuteCpuTask.java b/src/main/java/net/simon987/mar/npc/ExecuteCpuTask.java index 8176d38..4b36880 100644 --- a/src/main/java/net/simon987/mar/npc/ExecuteCpuTask.java +++ b/src/main/java/net/simon987/mar/npc/ExecuteCpuTask.java @@ -5,7 +5,7 @@ import net.simon987.mar.server.game.objects.Action; public class ExecuteCpuTask extends NPCTask { - private static final int MAX_EXEC_TIME = GameServer.INSTANCE.getConfig().getInt("npc_exec_time"); + private static final int MAX_EXEC_INSTRUCTIONS = GameServer.INSTANCE.getConfig().getInt("npc_exec_instructions"); @Override public boolean checkCompleted() { @@ -18,9 +18,10 @@ public class ExecuteCpuTask extends NPCTask { HackedNPC hNpc = (HackedNPC) npc; //Execute code - int timeout = Math.min(hNpc.getEnergy(), MAX_EXEC_TIME); + int allocation = Math.min(hNpc.getEnergy() * 10000, MAX_EXEC_INSTRUCTIONS); hNpc.getCpu().reset(); - int cost = hNpc.getCpu().execute(timeout); + hNpc.getCpu().setInstructionAlloction(allocation); + int cost = hNpc.getCpu().execute(); hNpc.spendEnergy(cost); if (hNpc.getCurrentAction() == Action.WALKING) { diff --git a/src/main/java/net/simon987/mar/server/GameServer.java b/src/main/java/net/simon987/mar/server/GameServer.java index 494b2af..0f759ab 100644 --- a/src/main/java/net/simon987/mar/server/GameServer.java +++ b/src/main/java/net/simon987/mar/server/GameServer.java @@ -22,6 +22,7 @@ import net.simon987.mar.npc.event.VaultCompleteListener; import net.simon987.mar.npc.event.VaultWorldUpdateListener; import net.simon987.mar.npc.world.TileVaultFloor; import net.simon987.mar.npc.world.TileVaultWall; +import net.simon987.mar.server.assembly.CPU; import net.simon987.mar.server.crypto.CryptoProvider; import net.simon987.mar.server.crypto.SecretKeyGenerator; import net.simon987.mar.server.event.*; @@ -41,6 +42,9 @@ import org.bson.Document; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; public class GameServer implements Runnable { @@ -53,7 +57,7 @@ public class GameServer implements Runnable { private SocketServer socketServer; - private final int maxExecutionTime; + private final int maxExecutionInstructions; private final DayNightCycle dayNightCycle; @@ -69,6 +73,8 @@ public class GameServer implements Runnable { private String secretKey; + public final ReadWriteLock execLock; + public GameServer() { this.config = new ServerConfiguration("config.properties"); @@ -86,7 +92,7 @@ public class GameServer implements Runnable { gameUniverse.setMongo(mongo); gameRegistry = new GameRegistry(); - maxExecutionTime = config.getInt("user_timeout"); + maxExecutionInstructions = config.getInt("user_instructions_per_tick"); cryptoProvider = new CryptoProvider(); @@ -104,7 +110,7 @@ public class GameServer implements Runnable { registerEventListeners(); registerGameObjects(); - + execLock = new ReentrantReadWriteLock(); } private void registerEventListeners() { @@ -266,12 +272,14 @@ public class GameServer implements Runnable { if (user.getControlledUnit() != null && user.getControlledUnit().getCpu() != null) { try { - int timeout = Math.min(user.getControlledUnit().getEnergy(), maxExecutionTime); + CPU cpu = user.getControlledUnit().getCpu(); + int allocation = Math.min(user.getControlledUnit().getEnergy() * CPU.INSTRUCTION_COST, maxExecutionInstructions); + cpu.setInstructionAlloction(allocation); - user.getControlledUnit().getCpu().reset(); - int cost = user.getControlledUnit().getCpu().execute(timeout); - user.getControlledUnit().spendEnergy(cost); - user.addTime(cost); + if (!cpu.isPaused()) { + cpu.reset(); + executeUserCode(user); + } } catch (Exception e) { LogManager.LOGGER.severe("Error executing " + user.getUsername() + "'s code"); @@ -281,6 +289,7 @@ public class GameServer implements Runnable { } //Process each worlds + GameServer.INSTANCE.execLock.writeLock().lock(); for (World world : gameUniverse.getWorlds()) { if (world.shouldUpdate()) { world.update(); @@ -291,10 +300,23 @@ public class GameServer implements Runnable { if (gameUniverse.getTime() % config.getInt("save_interval") == 0) { save(); } + GameServer.INSTANCE.execLock.writeLock().unlock(); socketServer.tick(); } + public void executeUserCode(User user) { + GameServer.INSTANCE.execLock.readLock().lock(); + int cost = user.getControlledUnit().getCpu().execute(); + user.getControlledUnit().spendEnergy(cost); + user.addTime(cost); + GameServer.INSTANCE.execLock.readLock().unlock(); + + if (user.getControlledUnit().getCpu().isPaused()) { + socketServer.promptUserPausedState(user); + } + } + void load() { LogManager.LOGGER.info("Loading all data from MongoDB"); diff --git a/src/main/java/net/simon987/mar/server/assembly/Assembler.java b/src/main/java/net/simon987/mar/server/assembly/Assembler.java index de8b119..36c8ae3 100755 --- a/src/main/java/net/simon987/mar/server/assembly/Assembler.java +++ b/src/main/java/net/simon987/mar/server/assembly/Assembler.java @@ -61,7 +61,6 @@ public class Assembler { * @return The line without its label part */ private static String removeLabel(String line) { - return line.replaceAll(labelPattern, ""); } @@ -246,7 +245,7 @@ public class Assembler { int factor = Integer.decode(valueTokens[0]); if (factor > MEM_SIZE) { - throw new InvalidOperandException("Factor '"+factor+"' exceeds total memory size", currentLine); + throw new InvalidOperandException("Factor '" + factor + "' exceeds total memory size", currentLine); } String value = valueTokens[1].substring(4, valueTokens[1].lastIndexOf(')')); @@ -285,14 +284,15 @@ public class Assembler { * * @param line Current line */ - private static void checkForSectionDeclaration(String line, AssemblyResult result, - int currentLine, int currentOffset) throws AssemblyException { + private void checkForSectionDeclaration(String line, AssemblyResult result, + int currentLine, int currentOffset) throws AssemblyException { String[] tokens = line.split("\\s+"); if (tokens[0].toUpperCase().equals(".TEXT")) { result.defineSection(Section.TEXT, currentLine, currentOffset); + result.disassemblyLines.add(".text"); throw new PseudoInstructionException(currentLine); } else if (tokens[0].toUpperCase().equals(".DATA")) { @@ -417,10 +417,10 @@ public class Assembler { int currentOffset = 0; for (int currentLine = 0; currentLine < lines.length; currentLine++) { try { - checkForLabel(lines[currentLine], result, (char)currentOffset); + checkForLabel(lines[currentLine], result, (char) currentOffset); //Increment offset - currentOffset += parseInstruction(lines[currentLine], currentLine, instructionSet).length / 2; + currentOffset += parseInstruction(result, lines[currentLine], currentLine, currentOffset, instructionSet).length / 2; if (currentOffset >= MEM_SIZE) { throw new OffsetOverflowException(currentOffset, MEM_SIZE, currentLine); @@ -455,8 +455,15 @@ public class Assembler { checkForEQUInstruction(line, result.labels, currentLine); checkForORGInstruction(line, result, currentLine); + for (String label : result.labels.keySet()) { + if (result.labels.get(label) == result.origin + currentOffset) { + result.disassemblyLines.add(String.format(" %s:", label)); + } + } + //Encode instruction - byte[] bytes = parseInstruction(line, currentLine, result.labels, instructionSet); + byte[] bytes = parseInstruction(result, line, currentLine, result.origin + currentOffset, result.labels, instructionSet); + result.codeLineMap.put(result.origin + currentOffset, result.disassemblyLines.size() - 1); currentOffset += bytes.length / 2; if (currentOffset >= MEM_SIZE) { @@ -487,8 +494,8 @@ public class Assembler { * @param currentLine Current line * @return The encoded instruction */ - private byte[] parseInstruction(String line, int currentLine, InstructionSet instructionSet) throws AssemblyException { - return parseInstruction(line, currentLine, null, instructionSet, true); + private byte[] parseInstruction(AssemblyResult result, String line, int currentLine, int offset, InstructionSet instructionSet) throws AssemblyException { + return parseInstruction(result, line, currentLine, offset, null, instructionSet, true); } /** @@ -499,10 +506,10 @@ public class Assembler { * @param labels List of labels * @return The encoded instruction */ - private byte[] parseInstruction(String line, int currentLine, HashMap labels, - InstructionSet instructionSet) + private byte[] parseInstruction(AssemblyResult result, String line, int currentLine, int offset, + HashMap labels, InstructionSet instructionSet) throws AssemblyException { - return parseInstruction(line, currentLine, labels, instructionSet, false); + return parseInstruction(result, line, currentLine, offset, labels, instructionSet, false); } /** @@ -514,7 +521,7 @@ public class Assembler { * @param assumeLabels Assume that unknown operands are labels * @return The encoded instruction */ - private byte[] parseInstruction(String line, int currentLine, HashMap labels, + private byte[] parseInstruction(AssemblyResult result, String line, int currentLine, int offset, HashMap labels, InstructionSet instructionSet, boolean assumeLabels) throws AssemblyException { @@ -555,6 +562,8 @@ public class Assembler { throw new InvalidMnemonicException(mnemonic, currentLine); } + StringBuilder disassembly = new StringBuilder(); + //Check operands and encode instruction final int beginIndex = line.indexOf(mnemonic) + mnemonic.length(); if (line.contains(",")) { @@ -576,6 +585,17 @@ public class Assembler { //Get instruction by name instructionSet.get(mnemonic).encode(out, o1, o2, currentLine); + if (!assumeLabels) { + byte[] bytes = out.toByteArray(); + for (int i = 0; i < bytes.length; i += 2) { + disassembly.append(String.format("%02X%02X ", bytes[i], bytes[i + 1])); + } + result.disassemblyLines.add(String.format( + "%04X %-15s %s %s, %s", offset, disassembly, mnemonic.toUpperCase(), + o1.toString(registerSet), o2.toString(registerSet) + )); + } + } else if (tokens.length > 1) { //1 operand @@ -591,12 +611,32 @@ public class Assembler { //Encode instruction //Get instruction by name instructionSet.get(mnemonic).encode(out, o1, currentLine); + + if (!assumeLabels) { + byte[] bytes = out.toByteArray(); + for (int i = 0; i < bytes.length; i += 2) { + disassembly.append(String.format("%02X%02X ", bytes[i], bytes[i + 1])); + } + result.disassemblyLines.add(String.format( + "%04X %-15s %s %s", offset, disassembly, mnemonic.toUpperCase(), o1.toString(registerSet) + )); + } } else { //No operand //Encode instruction //Get instruction by name instructionSet.get(mnemonic).encode(out, currentLine); + + if (!assumeLabels) { + byte[] bytes = out.toByteArray(); + for (int i = 0; i < bytes.length; i += 2) { + disassembly.append(String.format("%02X%02X ", bytes[i], bytes[i + 1])); + } + result.disassemblyLines.add(String.format( + "%04X %-15s %s", offset, disassembly, mnemonic.toUpperCase() + )); + } } return out.toByteArray(); diff --git a/src/main/java/net/simon987/mar/server/assembly/AssemblyResult.java b/src/main/java/net/simon987/mar/server/assembly/AssemblyResult.java index 5e6f5a2..1054491 100755 --- a/src/main/java/net/simon987/mar/server/assembly/AssemblyResult.java +++ b/src/main/java/net/simon987/mar/server/assembly/AssemblyResult.java @@ -9,6 +9,8 @@ import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayList; import java.util.HashMap; +import java.util.List; +import java.util.Map; /** * Result of an assembly attempt @@ -59,8 +61,14 @@ public class AssemblyResult { */ private boolean dataSectionSet = false; + public final Map codeLineMap; + + public List disassemblyLines; + AssemblyResult(IServerConfiguration config) { origin = config.getInt("org_offset"); + codeLineMap = new HashMap<>(); + disassemblyLines = new ArrayList<>(); } /** diff --git a/src/main/java/net/simon987/mar/server/assembly/CPU.java b/src/main/java/net/simon987/mar/server/assembly/CPU.java index cf3fd2f..551a98f 100755 --- a/src/main/java/net/simon987/mar/server/assembly/CPU.java +++ b/src/main/java/net/simon987/mar/server/assembly/CPU.java @@ -20,11 +20,10 @@ import org.bson.Document; */ public class CPU implements MongoSerializable { - /** - * - */ private final Status status; + private boolean trapFlag = false; + /** * Memory associated with the CPU, 64kb max */ @@ -50,12 +49,15 @@ public class CPU implements MongoSerializable { private final int graceInstructionCount; private int graceInstructionsLeft; private boolean isGracePeriod; + private int instructionAllocation; /** * Instruction pointer, always points to the next instruction */ private int ip; + public static final int INSTRUCTION_COST = 10000; + /** * Hardware is connected to the hardwareHost */ @@ -132,6 +134,12 @@ public class CPU implements MongoSerializable { * Sets the IP to IVT + number and pushes flags then the old IP */ public void interrupt(int number) { + + if (number == 3) { + trapFlag = true; + return; + } + Instruction push = instructionSet.get(PushInstruction.OPCODE); push.execute(status.toWord(), status); push.execute(ip, status); @@ -144,11 +152,15 @@ public class CPU implements MongoSerializable { ip = codeSectionOffset; graceInstructionsLeft = graceInstructionCount; isGracePeriod = false; + trapFlag = false; } - public int execute(int timeout) { + public void setInstructionAlloction(int instructionAllocation) { + this.instructionAllocation = instructionAllocation; + } + + public int execute() { - long startTime = System.currentTimeMillis(); int counter = 0; status.clear(); @@ -158,44 +170,52 @@ public class CPU implements MongoSerializable { counter++; if (isGracePeriod) { - if (graceInstructionsLeft-- == 0) { - writeExecutionStats(timeout, counter); - return timeout; - } - } else if (counter % 10000 == 0) { - if (System.currentTimeMillis() > (startTime + timeout)) { - interrupt(IntInstruction.INT_EXEC_LIMIT_REACHED); - isGracePeriod = true; + if (graceInstructionsLeft-- <= 0) { + writeExecutionStats(counter); + return counter / INSTRUCTION_COST; } + } else if (instructionAllocation-- <= 0) { + interrupt(IntInstruction.INT_EXEC_LIMIT_REACHED); + isGracePeriod = true; } - //fetch instruction - int machineCode = memory.get(ip); + step(); - /* - * Contents of machineCode should look like this: - * SSSS SDDD DDOO OOOO - * Where S is source, D is destination and O is the opCode - */ - Instruction instruction = instructionSet.get(machineCode & 0x03F); // 0000 0000 00XX XXXX - - int source = (machineCode >> 11) & 0x001F; // XXXX X000 0000 0000 - int destination = (machineCode >> 6) & 0x001F; // 0000 0XXX XX00 0000 - - executeInstruction(instruction, source, destination); -// LogManager.LOGGER.info(instruction.getMnemonic()); + if (trapFlag) { + break; + } } - int elapsed = (int) (System.currentTimeMillis() - startTime); // LogManager.LOGGER.fine(counter + " instruction in " + elapsed + "ms : " + (double) counter / (elapsed / 1000) / 1000000 + "MHz"); - writeExecutionStats(elapsed, counter); + writeExecutionStats(counter); - return elapsed; + return counter / INSTRUCTION_COST; } - private void writeExecutionStats(int timeout, int counter) { - memory.set(EXECUTION_COST_ADDR, timeout); + public void step() { + //fetch instruction + int machineCode = memory.get(ip); + + /* + * Contents of machineCode should look like this: + * SSSS SDDD DDOO OOOO + * Where S is source, D is destination and O is the opCode + */ + Instruction instruction = instructionSet.get(machineCode & 0x03F); // 0000 0000 00XX XXXX + + int source = (machineCode >> 11) & 0x001F; // XXXX X000 0000 0000 + int destination = (machineCode >> 6) & 0x001F; // 0000 0XXX XX00 0000 + + executeInstruction(instruction, source, destination); +// LogManager.LOGGER.info(instruction.getMnemonic()); + + if (status.isBreakFlag()) { + trapFlag = false; + } + } + + private void writeExecutionStats(int counter) { memory.set(EXECUTED_INS_ADDR, Util.getHigherWord(counter)); memory.set(EXECUTED_INS_ADDR + 1, Util.getLowerWord(counter)); } @@ -458,7 +478,6 @@ public class CPU implements MongoSerializable { @Override public String toString() { - String str = registerSet.toString(); str += status.toString(); @@ -483,4 +502,12 @@ public class CPU implements MongoSerializable { status.clone() ); } + + public boolean isPaused() { + return trapFlag; + } + + public void setTrapFlag(boolean trapFlag) { + this.trapFlag = trapFlag; + } } diff --git a/src/main/java/net/simon987/mar/server/assembly/Memory.java b/src/main/java/net/simon987/mar/server/assembly/Memory.java index 7c2cf26..848d05e 100755 --- a/src/main/java/net/simon987/mar/server/assembly/Memory.java +++ b/src/main/java/net/simon987/mar/server/assembly/Memory.java @@ -195,4 +195,31 @@ public class Memory implements Target, MongoSerializable, Cloneable { System.arraycopy(words, 0, memory.words, 0, words.length); return memory; } + + @Override + public String toString() { + StringBuilder result = new StringBuilder(); + + result.append(" "); + for (int i = 0; i < 16; i++) { + result.append(String.format("%4X ", i)); + } + result.append("\n"); + result.append(" 0 "); + + int count = 1; + for (int i = 0; i < words.length; i++) { + result.append(String.format("%04X ", (int) words[i])); + if (count == 16) { + count = 0; + result.append("\n"); + if (i + 1 != words.length) { + result.append(String.format("%4X ", i + 1)); + } + } + count++; + } + + return result.toString(); + } } diff --git a/src/main/java/net/simon987/mar/server/assembly/Operand.java b/src/main/java/net/simon987/mar/server/assembly/Operand.java index fb13ece..8e629f3 100755 --- a/src/main/java/net/simon987/mar/server/assembly/Operand.java +++ b/src/main/java/net/simon987/mar/server/assembly/Operand.java @@ -264,6 +264,22 @@ public class Operand { } catch (NumberFormatException e2) { return false; } + } else if (expr.startsWith("+0o")) { + try { + data = Integer.parseInt(expr.substring(3), 8); + value += registerSet.size() * 2; //refers to memory with disp + return true; + } catch (NumberFormatException e2) { + return false; + } + } else if (expr.startsWith("-0o")) { + try { + data = -Integer.parseInt(expr.substring(3), 8); + value += registerSet.size() * 2; //refers to memory with disp + return true; + } catch (NumberFormatException e2) { + return false; + } } return false; @@ -281,4 +297,20 @@ public class Operand { public int getData() { return data; } + + public String toString(RegisterSet registerSet) { + switch (type) { + case REGISTER16: + return registerSet.getRegister(value).getName(); + case MEMORY_IMM16: + return String.format("[%04X]", data); + case MEMORY_REG16: + return String.format("[%s]", registerSet.getRegister(value - registerSet.size()).getName()); + case MEMORY_REG_DISP16: + return String.format("[%s + %04X]", registerSet.getRegister(value - registerSet.size() * 2).getName(), data); + case IMMEDIATE16: + return String.format("%04X", data); + } + return null; + } } diff --git a/src/main/java/net/simon987/mar/server/assembly/RegisterSet.java b/src/main/java/net/simon987/mar/server/assembly/RegisterSet.java index f2e7595..f410161 100755 --- a/src/main/java/net/simon987/mar/server/assembly/RegisterSet.java +++ b/src/main/java/net/simon987/mar/server/assembly/RegisterSet.java @@ -185,13 +185,20 @@ public class RegisterSet implements Target, MongoSerializable, Cloneable { @Override public String toString() { - String str = ""; + StringBuilder sb = new StringBuilder(); for (int i = 1; i <= size; i++) { - str += i + " " + getRegister(i).getName() + "=" + Util.toHex(getRegister(i).getValue()) + "\n"; + Register reg = getRegister(i); + sb.append(reg.getName()); + sb.append("="); + if (i == size) { + sb.append(String.format("%04X", (int)reg.getValue())); + } else { + sb.append(String.format("%04X ", (int)reg.getValue())); + } } - return str; + return sb.toString(); } @Override diff --git a/src/main/java/net/simon987/mar/server/assembly/Status.java b/src/main/java/net/simon987/mar/server/assembly/Status.java index 6b4400b..8c7f271 100755 --- a/src/main/java/net/simon987/mar/server/assembly/Status.java +++ b/src/main/java/net/simon987/mar/server/assembly/Status.java @@ -56,10 +56,14 @@ public class Status implements Cloneable { } public String toString() { - return "" + (signFlag ? 1 : 0) + ' ' + - (zeroFlag ? 1 : 0) + ' ' + - (carryFlag ? 1 : 0) + ' ' + - (overflowFlag ? 1 : 0) + '\n'; + return String.format( + "C=%d Z=%s S=%s O=%s B=%s", + carryFlag ? 1 : 0, + zeroFlag ? 1 : 0, + signFlag ? 1 : 0, + overflowFlag ? 1 : 0, + breakFlag ? 1 : 0 + ); } /** diff --git a/src/main/java/net/simon987/mar/server/assembly/Util.java b/src/main/java/net/simon987/mar/server/assembly/Util.java index 4680afd..4fcd42a 100755 --- a/src/main/java/net/simon987/mar/server/assembly/Util.java +++ b/src/main/java/net/simon987/mar/server/assembly/Util.java @@ -47,20 +47,20 @@ public class Util { public static String toHex(byte[] byteArray) { - String result = ""; + StringBuilder result = new StringBuilder(); int count = 0; for (byte b : byteArray) { - result += String.format("%02X ", b); + result.append(String.format("%02X ", b)); if (count == 16) { count = -1; - result += "\n"; + result.append("\n"); } count++; } - return result; + return result.toString(); } /** diff --git a/src/main/java/net/simon987/mar/server/user/User.java b/src/main/java/net/simon987/mar/server/user/User.java index 7b59cc5..8def0e5 100755 --- a/src/main/java/net/simon987/mar/server/user/User.java +++ b/src/main/java/net/simon987/mar/server/user/User.java @@ -8,6 +8,8 @@ import net.simon987.mar.server.game.objects.ControllableUnit; import net.simon987.mar.server.io.MongoSerializable; import org.bson.Document; +import java.util.*; + public class User implements MongoSerializable { private String username; @@ -23,6 +25,10 @@ public class User implements MongoSerializable { private UserStats stats; + private Map codeLineMap; + + private List disassemblyLines; + public User() throws CancelledException { GameEvent event = new UserCreationEvent(this); GameServer.INSTANCE.getEventDispatcher().dispatch(event); @@ -49,6 +55,16 @@ public class User implements MongoSerializable { dbObject.put("password", password); dbObject.put("moderator", moderator); dbObject.put("stats", stats.mongoSerialise()); + dbObject.put("disassembly", disassemblyLines); + + List> codeLineList = new ArrayList<>(); + if (codeLineMap != null) { + for (int offset: codeLineMap.keySet()) { + codeLineList.add(Arrays.asList(offset, codeLineMap.get(offset))); + } + } + dbObject.put("codeLineMap", codeLineList); + return dbObject; } @@ -59,10 +75,20 @@ public class User implements MongoSerializable { user.getControlledUnit().setParent(user); user.username = (String) obj.get("username"); user.userCode = (String) obj.get("code"); + user.password = (String) obj.get("password"); user.moderator = (boolean) obj.get("moderator"); user.stats = new UserStats((Document) obj.get("stats")); + List> codeLineList = (List>) obj.get("codeLineMap"); + if (codeLineList != null) { + user.codeLineMap = new HashMap<>(codeLineList.size()); + for (List tuple: codeLineList) { + user.codeLineMap.put(tuple.get(0), tuple.get(1)); + } + } + user.disassemblyLines = (List) obj.get("disassembly"); + return user; } @@ -78,6 +104,14 @@ public class User implements MongoSerializable { this.userCode = userCode; } + public void setCodeLineMap(Map codeLineMap) { + this.codeLineMap = codeLineMap; + } + + public Map getCodeLineMap() { + return codeLineMap; + } + public String getUsername() { return username; } @@ -125,4 +159,27 @@ public class User implements MongoSerializable { public UserStats getStats() { return stats; } + + public List getDisassembly() { + return disassemblyLines; + } + + public void setDisassembly(List disassemblyLines) { + this.disassemblyLines = disassemblyLines; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + User user = (User) o; + + return username.equals(user.username); + } + + @Override + public int hashCode() { + return username.hashCode(); + } } diff --git a/src/main/java/net/simon987/mar/server/websocket/CodeUploadHandler.java b/src/main/java/net/simon987/mar/server/websocket/CodeUploadHandler.java index 929580a..1e17bc4 100644 --- a/src/main/java/net/simon987/mar/server/websocket/CodeUploadHandler.java +++ b/src/main/java/net/simon987/mar/server/websocket/CodeUploadHandler.java @@ -29,6 +29,9 @@ public class CodeUploadHandler implements MessageHandler { cpu.getRegisterSet(), GameServer.INSTANCE.getConfig()).parse(user.getUser().getUserCode()); + user.getUser().setCodeLineMap(ar.codeLineMap); + user.getUser().setDisassembly(ar.disassemblyLines); + cpu.getMemory().clear(); //Write assembled code to mem @@ -37,15 +40,13 @@ public class CodeUploadHandler implements MessageHandler { cpu.getMemory().write((char) ar.origin, assembledCode, 0, assembledCode.length); cpu.setCodeSectionOffset(ar.getCodeSectionOffset()); + cpu.reset(); //Clear keyboard buffer if (user.getUser().getControlledUnit() != null && user.getUser().getControlledUnit().getKeyboardBuffer() != null) { user.getUser().getControlledUnit().getKeyboardBuffer().clear(); } - //Clear registers - cpu.getRegisterSet().clear(); - JSONObject response = new JSONObject(); response.put("t", "codeResponse"); response.put("bytes", ar.bytes.length); diff --git a/src/main/java/net/simon987/mar/server/websocket/DebugStepHandler.java b/src/main/java/net/simon987/mar/server/websocket/DebugStepHandler.java new file mode 100644 index 0000000..bd9417c --- /dev/null +++ b/src/main/java/net/simon987/mar/server/websocket/DebugStepHandler.java @@ -0,0 +1,56 @@ +package net.simon987.mar.server.websocket; + +import net.simon987.mar.server.GameServer; +import net.simon987.mar.server.assembly.CPU; +import net.simon987.mar.server.logging.LogManager; +import org.json.simple.JSONObject; + +import java.io.IOException; +import java.util.Map; + +public class DebugStepHandler implements MessageHandler { + + public static String pausedStatePrompt(Integer line, boolean stateSent) { + JSONObject response = new JSONObject(); + response.put("t", "paused"); + response.put("line", line == null ? -1 : line); + response.put("stateSent", stateSent); + return response.toJSONString(); + } + + @Override + public void handle(OnlineUser user, JSONObject json) throws IOException { + + if (json.get("t").equals("debugStep")) { + + LogManager.LOGGER.fine("(WS) Debug step from " + user.getUser().getUsername()); + + if (user.getUser().isGuest()) { + return; + } + + CPU cpu = user.getUser().getControlledUnit().getCpu(); + + if (!cpu.isPaused()) { + return; + } + + if (json.get("mode").equals("step")) { + GameServer.INSTANCE.execLock.readLock().lock(); + cpu.step(); + GameServer.INSTANCE.execLock.readLock().unlock(); + + Map lineMap = user.getUser().getCodeLineMap(); + Integer line = lineMap.get(cpu.getIp()); + + // Automatically send state when stepping through code to reduce latency + user.getWebSocket().getRemote().sendString(pausedStatePrompt(line, true)); + StateRequestHandler.sendState(user); + + } else if (json.get("mode").equals("continue")) { + cpu.setTrapFlag(false); + GameServer.INSTANCE.executeUserCode(user.getUser()); + } + } + } +} diff --git a/src/main/java/net/simon987/mar/server/websocket/DisassemblyRequestHandler.java b/src/main/java/net/simon987/mar/server/websocket/DisassemblyRequestHandler.java new file mode 100644 index 0000000..1a6b2f1 --- /dev/null +++ b/src/main/java/net/simon987/mar/server/websocket/DisassemblyRequestHandler.java @@ -0,0 +1,28 @@ +package net.simon987.mar.server.websocket; + +import net.simon987.mar.server.logging.LogManager; +import org.json.simple.JSONObject; + +import java.io.IOException; + +public class DisassemblyRequestHandler implements MessageHandler { + @Override + public void handle(OnlineUser user, JSONObject json) throws IOException { + + if (json.get("t").equals("disassemblyRequest")) { + + LogManager.LOGGER.fine("(WS) Disassembly request from " + user.getUser().getUsername()); + + if (user.getUser().isGuest()) { + return; + } + + JSONObject response = new JSONObject(); + + response.put("t", "disassembly"); + response.put("lines", user.getUser().getDisassembly()); + + user.getWebSocket().getRemote().sendString(response.toJSONString()); + } + } +} diff --git a/src/main/java/net/simon987/mar/server/websocket/OnlineUserManager.java b/src/main/java/net/simon987/mar/server/websocket/OnlineUserManager.java index 6ad852c..997fe78 100644 --- a/src/main/java/net/simon987/mar/server/websocket/OnlineUserManager.java +++ b/src/main/java/net/simon987/mar/server/websocket/OnlineUserManager.java @@ -1,5 +1,6 @@ package net.simon987.mar.server.websocket; +import net.simon987.mar.server.user.User; import org.eclipse.jetty.websocket.api.Session; import java.util.ArrayList; @@ -23,6 +24,21 @@ public class OnlineUserManager { return null; } + public List getUser(User user) { + + List _onlineUsers = new ArrayList<>(onlineUsers); + + List result = new ArrayList<>(); + + for (OnlineUser onlineUser : _onlineUsers) { + if (onlineUser.getUser().equals(user)) { + result.add(onlineUser); + } + } + + return result; + } + /** * Add an user to the list * diff --git a/src/main/java/net/simon987/mar/server/websocket/SocketServer.java b/src/main/java/net/simon987/mar/server/websocket/SocketServer.java index 6b1e151..0c02dd7 100644 --- a/src/main/java/net/simon987/mar/server/websocket/SocketServer.java +++ b/src/main/java/net/simon987/mar/server/websocket/SocketServer.java @@ -16,6 +16,7 @@ import org.json.simple.JSONObject; import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.Map; @WebSocket(maxTextMessageSize = SocketServer.MAX_TXT_MESSAGE_SIZE) public class SocketServer { @@ -44,6 +45,9 @@ public class SocketServer { messageDispatcher.addHandler(new CodeRequestHandler()); messageDispatcher.addHandler(new KeypressHandler()); messageDispatcher.addHandler(new DebugCommandHandler()); + messageDispatcher.addHandler(new StateRequestHandler()); + messageDispatcher.addHandler(new DisassemblyRequestHandler()); + messageDispatcher.addHandler(new DebugStepHandler()); } @OnWebSocketConnect @@ -117,6 +121,10 @@ public class SocketServer { onlineUser.setAuthenticated(true); sendString(session, AUTH_OK_MESSAGE); + + if (user.getControlledUnit().getCpu().isPaused()) { + promptUserPausedState(user); + } } /** @@ -186,4 +194,13 @@ public class SocketServer { return jsonInts; } + + public void promptUserPausedState(User user) { + for (OnlineUser onlineUser : onlineUserManager.getUser(user)) { + Map lineMap = user.getCodeLineMap(); + Integer line = lineMap.get(user.getControlledUnit().getCpu().getIp()); + + sendString(onlineUser.getWebSocket(), DebugStepHandler.pausedStatePrompt(line, false)); + } + } } diff --git a/src/main/java/net/simon987/mar/server/websocket/StateRequestHandler.java b/src/main/java/net/simon987/mar/server/websocket/StateRequestHandler.java new file mode 100644 index 0000000..2e3f4ea --- /dev/null +++ b/src/main/java/net/simon987/mar/server/websocket/StateRequestHandler.java @@ -0,0 +1,45 @@ +package net.simon987.mar.server.websocket; + +import net.simon987.mar.server.assembly.CPU; +import net.simon987.mar.server.logging.LogManager; +import org.json.simple.JSONObject; + +import java.io.IOException; +import java.util.Map; + +public class StateRequestHandler implements MessageHandler { + @Override + public void handle(OnlineUser user, JSONObject json) throws IOException { + + if (json.get("t").equals("stateRequest")) { + + LogManager.LOGGER.fine("(WS) State request from " + user.getUser().getUsername()); + + if (user.getUser().isGuest()) { + return; + } + + sendState(user); + } + } + + public static void sendState(OnlineUser user) throws IOException { + JSONObject response = new JSONObject(); + + CPU cpu = user.getUser().getControlledUnit().getCpu(); + + if (!cpu.isPaused()) { + return; + } + + response.put("t", "state"); + response.put("memory", cpu.getMemory().toString()); + response.put("status", cpu.getStatus().toString()); + response.put("registers", cpu.getRegisterSet().toString()); + Map codeLineMap = user.getUser().getCodeLineMap(); + Integer line = codeLineMap == null ? null : codeLineMap.get(cpu.getIp()); + response.put("line", line == null ? 0 : line); + + user.getWebSocket().getRemote().sendString(response.toJSONString()); + } +} diff --git a/src/main/resources/config.properties b/src/main/resources/config.properties index 19404a9..a5b99c4 100644 --- a/src/main/resources/config.properties +++ b/src/main/resources/config.properties @@ -49,7 +49,7 @@ ivt_offset=0 grace_instruction_count=10000 stack_bottom=65536 memory_size=65536 -user_timeout=100 +user_instructions_per_tick=100000 #User creation new_user_worldX=32767 new_user_worldY=32767 @@ -80,7 +80,7 @@ harvester_regen=5 harvester_biomass_drop_count=8 radio_tower_range=3 hacked_npc_mem_size=8192 -npc_exec_time=5 +npc_exec_instructions=50000 hacked_npc_die_on_no_energy=1 #Vaults vault_door_open_time=4 diff --git a/src/main/resources/static/css/mar.css b/src/main/resources/static/css/mar.css index 6f79b40..27bbc98 100644 --- a/src/main/resources/static/css/mar.css +++ b/src/main/resources/static/css/mar.css @@ -321,3 +321,120 @@ -o-animation: rotating 2s linear infinite; animation: rotating 2s linear infinite; } + +#state-memory { + padding: 0.5em; + background: #2D2D2D; + text-align: right; + overflow-y: scroll; + max-height: 488px; + + display: block; + white-space: pre; + margin: 1em 0; + + font-size: 16px; + font-family: fixedsys, monospace; +} + +#state-disassembly { + padding: 0.5em 0 0.5em 0.5em; + background: #2D2D2D; + text-align: left; + overflow-y: auto; + max-height: 600px; + + display: block; + white-space: pre; + margin: 1em 0; + + font-size: 16px; + font-family: fixedsys, monospace; +} + +#state-registers { + display: block; + white-space: pre; + margin: 1em 0; + + font-size: 16px; + font-family: fixedsys, monospace; + text-align: right; +} + +#state-status { + display: block; + white-space: pre; + margin: 1em 0; + + font-size: 16px; + font-family: fixedsys, monospace; + text-align: right; +} + +#disassembly-hl { + background: #ff000077; + display: inline-block; + width: 100%; +} + +.i3 { + background: rgba(255, 0, 0, 0.10); + display: inline-block; + width: 100%; +} + +._0 { + color: #888; +} + +._l { + color: #6699CC; +} + +._a { + color: #fff; +} + +._k { + color: #CC99CC; +} + +._o { + color: #66CCCC; +} + +._b { + color: #F2777A; +} + +.col-grow { + padding: 0 1em; + flex-grow: 1; +} + +.col-shrink { + padding-right: 1em; +} + +#paused > .mi { + font-size: 16px; +} +#paused { + margin-left: 4px; +} + +::-webkit-scrollbar-track { + -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.2); + background-color: #FFFFFF33; +} + +::-webkit-scrollbar { + width: 8px; + background-color: #FFFFFF77; +} + +::-webkit-scrollbar-thumb { + -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, .3); + background-color: #0c0d16; +} diff --git a/src/main/resources/static/js/ace/mode-mar.js b/src/main/resources/static/js/ace/mode-mar.js index 05bed3d..6843a84 100644 --- a/src/main/resources/static/js/ace/mode-mar.js +++ b/src/main/resources/static/js/ace/mode-mar.js @@ -47,12 +47,12 @@ ace.define("ace/mode/mar_rules", ["require", "exports", "ace/lib/oop", "ace/mode start: [{ token: 'keyword.function.assembly', - regex: '\\b(?:mov|add|sub|and|or|test|cmp|shl|shr|mul|push|pop|div|xor|dw|nop|equ|neg|hwq|not|ror|rol|sal|sar|inc|dec|rcl|xchg|rcr|pushf|popf)\\b', + regex: '\\b(?:mov|add|sub|and|or|test|cmp|shl|shr|mul|push|pop|div|xor|dw|nop|equ|neg|hwq|not|ror|rol|sal|sar|inc|dec|rcl|xchg|rcr|pushf|popf|seta|setnbe|setae|setnb|setnc|setbe|setna|setb|setc|setnae|sete|setz|setne|setnz|setg|setnle|setge|setnl|setle|setng|setl|setnge|seto|setno|sets|setns)\\b', caseInsensitive: true }, { token: 'keyword.operator.assembly', - regex: '\\b(?:call|ret|jmp|jnz|jg|jl|jge|jle|hwi|jz|js|jns|jc|jnc|jo|jno|ja|jna|seta|setnbe|setae|setnb|setnc|setbe|setna|setb|setc|setnae|sete|setz|setne|setnz|setg|setnle|setge|setnl|setle|setng|setl|setnge|seto|setno|sets|setns)\\b', + regex: '\\b(?:call|ret|jmp|jnz|jg|jl|jge|jle|hwi|jz|js|jns|jc|jnc|jo|jno|ja|jna|int|iret|into)\\b', caseInsensitive: true }, { diff --git a/src/main/resources/static/js/editor.js b/src/main/resources/static/js/editor.js index d897207..f12a71a 100644 --- a/src/main/resources/static/js/editor.js +++ b/src/main/resources/static/js/editor.js @@ -27,10 +27,9 @@ function removeComment(line) { function checkForLabel(line, result) { line = removeComment(line); - var match; - if ((match = /^[a-zA-Z_]\w*:/.exec(line)) !== null) { - - result.labels.push(match[0].substring(0, match[0].length - 1)); + let match; + if ((match = /^\s*([a-zA-Z_]\w*):/.exec(line)) !== null) { + result.labels.push(match[1]); } } @@ -333,7 +332,7 @@ function parseInstruction(line, result, currentLine) { strO1 = line.substring(line.indexOf(mnemonic) + mnemonic.length).trim(); //Validate operand number - if (!new RegExp('\\b(?:push|mul|pop|div|neg|call|jnz|jg|jl|jge|jle|hwi|hwq|jz|js|jns|ret|jmp|not|jc|jnc|jo|jno|inc|dec|ja|jna|seta|setnbe|setae|setnb|setnc|setbe|setna|setb|setc|setnae|sete|setz|setne|setnz|setg|setnle|setge|setnl|setle|setng|setl|setnge|seto|setno|sets|setns)\\b').test(mnemonic.toLowerCase())) { + if (!new RegExp('\\b(?:push|mul|pop|div|neg|call|jnz|jg|jl|jge|jle|hwi|hwq|jz|js|jns|ret|jmp|not|jc|jnc|jo|jno|inc|dec|ja|jna|seta|setnbe|setae|setnb|setnc|setbe|setna|setb|setc|setnae|sete|setz|setne|setnz|setg|setnle|setge|setnl|setle|setng|setl|setnge|seto|setno|sets|setns|int)\\b').test(mnemonic.toLowerCase())) { result.annotations.push({ row: currentLine, column: 0, @@ -366,7 +365,7 @@ function parseInstruction(line, result, currentLine) { } else { //No operand - if (!new RegExp('\\b(?:ret|brk|nop|pushf|popf)\\b').test(mnemonic.toLowerCase())) { + if (!new RegExp('\\b(?:ret|brk|nop|pushf|popf|into|iret)\\b').test(mnemonic.toLowerCase())) { //Validate operand number result.annotations.push({ @@ -422,26 +421,34 @@ function parse() { editor.getSession().setAnnotations(result.annotations); } +function hideTabs() { + ["tab-world", "tab-world-sm", "tab-editor", "tab-editor-sm", "tab-debug", "tab-debug-sm"] + .forEach(tab => document.getElementById(tab).classList.remove("active")); + ["world-tab", "editor-tab", "debug-tab"] + .forEach(tab => document.getElementById(tab).setAttribute("style", "display: none")) +} + function tabWorldClick() { + hideTabs(); document.getElementById("tab-world").classList.add("active"); document.getElementById("tab-world-sm").classList.add("active"); - document.getElementById("tab-editor").classList.remove("active"); - document.getElementById("tab-editor-sm").classList.remove("active"); - document.getElementById("world-tab").setAttribute("style", ""); - document.getElementById("editor-tab").setAttribute("style", "display: none"); } function tabEditorClick() { - document.getElementById("tab-world").classList.remove("active"); - document.getElementById("tab-world-sm").classList.remove("active"); + hideTabs(); document.getElementById("tab-editor").classList.add("active"); document.getElementById("tab-editor-sm").classList.add("active"); - - document.getElementById("world-tab").setAttribute("style", "display: none"); document.getElementById("editor-tab").setAttribute("style", ""); } +function tabDebuggerClick() { + hideTabs(); + document.getElementById("tab-debug").classList.add("active"); + document.getElementById("tab-debug-sm").classList.add("active"); + document.getElementById("debug-tab").setAttribute("style", ""); +} + //----- //Check if browser supports local storage if not than bad luck, use something else than IE7 diff --git a/src/main/resources/templates/header.vm b/src/main/resources/templates/header.vm index bbcf5c7..90a2eb9 100644 --- a/src/main/resources/templates/header.vm +++ b/src/main/resources/templates/header.vm @@ -22,6 +22,16 @@ class="mi">account_circle Account + + + + -