mirror of
https://github.com/OpenMW/openmw.git
synced 2025-02-05 06:15:35 +00:00
423 lines
11 KiB
D
423 lines
11 KiB
D
|
/*
|
||
|
Monster - an advanced game scripting language
|
||
|
Copyright (C) 2007-2009 Nicolay Korslund
|
||
|
Email: <korslund@gmail.com>
|
||
|
WWW: http://monster.snaptoad.com/
|
||
|
|
||
|
This file (console.d) is part of the Monster script language package.
|
||
|
|
||
|
Monster is distributed as free software: you can redistribute it
|
||
|
and/or modify it under the terms of the GNU General Public License
|
||
|
version 3, as published by the Free Software Foundation.
|
||
|
|
||
|
This program is distributed in the hope that it will be useful, but
|
||
|
WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||
|
General Public License for more details.
|
||
|
|
||
|
You should have received a copy of the GNU General Public License
|
||
|
version 3 along with this program. If not, see
|
||
|
http://www.gnu.org/licenses/ .
|
||
|
|
||
|
*/
|
||
|
|
||
|
module monster.modules.console;
|
||
|
|
||
|
// This file implements an optimized way of running Monster
|
||
|
// interactively, what is sometimes called "line mode" or "interactive
|
||
|
// mode". It is ideally suited for making ingame consoles.
|
||
|
|
||
|
// The main idea is to retain all reusable data structures and
|
||
|
// minimize the number of heap allocations at runtime. The current
|
||
|
// implemention is not perfected in that regard, but later
|
||
|
// implementations will be.
|
||
|
|
||
|
// All input into the console is given through the input() function,
|
||
|
// and all output is sent to the output() callback function. You can
|
||
|
// also poll for output manually using output();
|
||
|
|
||
|
import monster.util.growarray;
|
||
|
import monster.compiler.tokenizer;
|
||
|
import monster.compiler.statement;
|
||
|
import monster.compiler.variables;
|
||
|
import monster.compiler.functions;
|
||
|
import monster.compiler.scopes;
|
||
|
import monster.compiler.bytecode;
|
||
|
import monster.compiler.assembler;
|
||
|
import monster.compiler.types;
|
||
|
import monster.compiler.expression;
|
||
|
import std.stdio;
|
||
|
import std.string;
|
||
|
import monster.monster;
|
||
|
|
||
|
// Console results
|
||
|
enum CR
|
||
|
{
|
||
|
Ok, // Command was executed
|
||
|
Error, // An error occurred
|
||
|
More, // An unterminated multi-line statement was entered, need
|
||
|
// more input
|
||
|
Empty, // The line was empty (nothing was executed)
|
||
|
}
|
||
|
|
||
|
class Console
|
||
|
{
|
||
|
private:
|
||
|
Tokenizer tn;
|
||
|
|
||
|
GrowArray!(Token) tokArr;
|
||
|
GrowArray!(char) outBuf;
|
||
|
|
||
|
Function fn;
|
||
|
|
||
|
FuncScope sc;
|
||
|
|
||
|
MonsterObject *obj;
|
||
|
|
||
|
Variable* varList[];
|
||
|
uint varSize;
|
||
|
|
||
|
// The thread that we run console commands in. It's put in the
|
||
|
// background when not in use.
|
||
|
Thread *trd;
|
||
|
|
||
|
// The thread that was running when we started (if any)
|
||
|
Thread *store;
|
||
|
|
||
|
int paren, curl, square;
|
||
|
|
||
|
void delegate(char[] str) output_cb;
|
||
|
bool hasCallback;
|
||
|
|
||
|
char[] norm_prompt = ">>> ";
|
||
|
int tab = 4;
|
||
|
char[] ml_prompt = "... ";
|
||
|
char[] cmt_prompt = "(comment) ";
|
||
|
|
||
|
public:
|
||
|
bool allowVar = false;
|
||
|
|
||
|
this(MonsterObject *ob = null)
|
||
|
{
|
||
|
tn = new Tokenizer();
|
||
|
|
||
|
// Set the context object
|
||
|
obj = ob;
|
||
|
if(obj is null)
|
||
|
obj = Function.getIntMO();
|
||
|
|
||
|
// Next set up the function and the scope
|
||
|
fn.name.str = "console";
|
||
|
fn.owner = obj.cls;
|
||
|
sc = new FuncScope(obj.cls.sc, &fn);
|
||
|
|
||
|
// Get a new thread
|
||
|
trd = Thread.getPaused();
|
||
|
}
|
||
|
|
||
|
private:
|
||
|
void put(char[] str, bool newLine=false)
|
||
|
{
|
||
|
if(hasCallback)
|
||
|
{
|
||
|
output_cb(str);
|
||
|
if(newLine)
|
||
|
output_cb("\n");
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
outBuf ~= str;
|
||
|
if(newLine)
|
||
|
outBuf ~= '\n';
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void putln(char[] str) { put(str, true); }
|
||
|
|
||
|
Statement parse(TokenArray toks)
|
||
|
{
|
||
|
Statement b = null;
|
||
|
|
||
|
if(VarDeclStatement.canParse(toks))
|
||
|
{
|
||
|
if(!allowVar) fail("Variable declaration not allowed here");
|
||
|
b = new VarDeclStatement;
|
||
|
}
|
||
|
else if(CodeBlock.canParse(toks)) b = new CodeBlock;
|
||
|
else if(IfStatement.canParse(toks)) b = new IfStatement;
|
||
|
else if(DoWhileStatement.canParse(toks)) b = new DoWhileStatement;
|
||
|
else if(WhileStatement.canParse(toks)) b = new WhileStatement;
|
||
|
else if(ForStatement.canParse(toks)) b = new ForStatement;
|
||
|
else if(ForeachStatement.canParse(toks)) b = new ForeachStatement;
|
||
|
else if(ImportStatement.canParse(toks)) b = new ImportStatement(true);
|
||
|
|
||
|
// If this is not one of the above, default to an expression
|
||
|
// statement.
|
||
|
else b = new ExprStatement;
|
||
|
|
||
|
b.parse(toks);
|
||
|
return b;
|
||
|
}
|
||
|
|
||
|
int sumParen()
|
||
|
{
|
||
|
// Clean up if we had an unmatched end bracket somewhere
|
||
|
if(paren < 0) paren = 0;
|
||
|
if(curl < 0) curl = 0;
|
||
|
if(square < 0) square = 0;
|
||
|
|
||
|
return paren + curl + square;
|
||
|
}
|
||
|
|
||
|
bool isComment()
|
||
|
{
|
||
|
return tn.mode != Tokenizer.Normal;
|
||
|
}
|
||
|
|
||
|
// Resets the console to a usable state. Does not delete variables.
|
||
|
void reset()
|
||
|
{
|
||
|
paren = 0;
|
||
|
curl = 0;
|
||
|
square = 0;
|
||
|
tn.mode = Tokenizer.Normal;
|
||
|
|
||
|
if(cthread is trd)
|
||
|
{
|
||
|
// Reset the function stack.
|
||
|
trd.fstack.killAll();
|
||
|
|
||
|
assert(trd.fstack.isEmpty);
|
||
|
assert(!trd.fstack.hasNatives);
|
||
|
|
||
|
// Our variables should still be on the stack though.
|
||
|
if(stack.getPos > varSize)
|
||
|
stack.popInts(stack.getPos - varSize);
|
||
|
else
|
||
|
assert(stack.getPos == varSize);
|
||
|
|
||
|
// Make sure the thread is still in the 'paused' mode
|
||
|
trd.moveTo(&scheduler.paused);
|
||
|
|
||
|
// Background the thread - this will also capture the stack
|
||
|
trd.background();
|
||
|
}
|
||
|
|
||
|
assert(trd !is cthread);
|
||
|
|
||
|
// Restore the previous thread (if any)
|
||
|
if(store !is null)
|
||
|
store.foreground();
|
||
|
}
|
||
|
|
||
|
// Push the input into the compiler and run it
|
||
|
CR runInput(char[] str)
|
||
|
{
|
||
|
// Set up the tokenizer
|
||
|
tn.setLine(str);
|
||
|
|
||
|
// Reset the token buffer, unless we're in a multiline command
|
||
|
if(paren == 0 && curl == 0 && square == 0)
|
||
|
tokArr.length = 0;
|
||
|
|
||
|
// Phase I, tokenize
|
||
|
Token t = tn.getNextFromLine();
|
||
|
// Mark the first token as a newline / separator
|
||
|
t.newline = true;
|
||
|
while(t.type != TT.EMPTY)
|
||
|
{
|
||
|
if(t.type == TT.LeftParen)
|
||
|
paren++;
|
||
|
else if(t.type == TT.LeftCurl)
|
||
|
curl++;
|
||
|
else if(t.type == TT.LeftSquare)
|
||
|
square++;
|
||
|
else if(t.type == TT.RightParen)
|
||
|
paren--;
|
||
|
else if(t.type == TT.RightCurl)
|
||
|
curl--;
|
||
|
else if(t.type == TT.RightSquare)
|
||
|
square--;
|
||
|
|
||
|
tokArr ~= t;
|
||
|
t = tn.getNextFromLine();
|
||
|
}
|
||
|
|
||
|
if(paren < 0 || curl < 0 || square < 0)
|
||
|
fail("Unmatched end bracket(s)");
|
||
|
|
||
|
// Wait for more input inside a bracket
|
||
|
if(sumParen() > 0)
|
||
|
return CR.More;
|
||
|
|
||
|
// Ditto for block comments
|
||
|
if(isComment())
|
||
|
return CR.More;
|
||
|
|
||
|
// Ignore empty token lists
|
||
|
if(tokArr.length == 0)
|
||
|
return CR.Empty;
|
||
|
|
||
|
// Phase II, parse
|
||
|
TokenArray toks = tokArr.arrayCopy();
|
||
|
Statement st = parse(toks);
|
||
|
delete toks;
|
||
|
|
||
|
// Phase III, resolve
|
||
|
st.resolve(sc);
|
||
|
|
||
|
// Phase IV, compile
|
||
|
tasm.newFunc();
|
||
|
|
||
|
// Is it an expression?
|
||
|
auto es = cast(ExprStatement)st;
|
||
|
if(es !is null)
|
||
|
{
|
||
|
// Yes. But is the type usable?
|
||
|
if(es.left.type.canCastTo(ArrayType.getString())
|
||
|
&& es.right is null)
|
||
|
{
|
||
|
// Yup. Get the type, and cast the expression to string.
|
||
|
scope auto ce = new CastExpression(es.left, ArrayType.getString());
|
||
|
ce.eval();
|
||
|
}
|
||
|
else es = null;
|
||
|
}
|
||
|
|
||
|
// No expression is being used, so compile the statement
|
||
|
if(es is null)
|
||
|
st.compile();
|
||
|
|
||
|
fn.bcode = tasm.assemble(fn.lines);
|
||
|
fn.bcode ~= cast(ubyte)BC.Exit; // Non-optimal hack
|
||
|
|
||
|
// Phase V, call the function
|
||
|
|
||
|
// First, background the current thread (if any) and bring up
|
||
|
// our own.
|
||
|
store = cthread;
|
||
|
if(store !is null)
|
||
|
store.background();
|
||
|
assert(trd !is null);
|
||
|
trd.foreground();
|
||
|
|
||
|
// We have to push ourselves on the function stack, or call()
|
||
|
// will see that it's empty and kill the thread upon exit
|
||
|
trd.fstack.pushExt("Console");
|
||
|
|
||
|
fn.call(obj);
|
||
|
|
||
|
trd.fstack.pop();
|
||
|
|
||
|
// Finally, get the expression result, if any, and print it
|
||
|
if(es !is null)
|
||
|
putln(stack.popString8());
|
||
|
|
||
|
// In the case of new variable declarations, we have to make
|
||
|
// sure they are accessible to any subsequent calls to the
|
||
|
// function. Since the stack frame gets set at each call, we
|
||
|
// have to access the variables outside the function frame,
|
||
|
// ie. the same way we treat function parameters. We do this by
|
||
|
// giving the variables negative indices.
|
||
|
auto vs = cast(VarDeclStatement)st;
|
||
|
if(vs !is null)
|
||
|
{
|
||
|
// Add the new vars to the list
|
||
|
foreach(v; vs.vars)
|
||
|
{
|
||
|
varList ~= v.var;
|
||
|
|
||
|
// Add the size as well
|
||
|
varSize += v.var.type.getSize;
|
||
|
}
|
||
|
|
||
|
// Recalculate all the indices backwards from zero
|
||
|
int place = 0;
|
||
|
foreach_reverse(v; varList)
|
||
|
{
|
||
|
place -= v.type.getSize;
|
||
|
v.number = place;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Reset the console to a usable state
|
||
|
reset();
|
||
|
|
||
|
return CR.Ok;
|
||
|
}
|
||
|
|
||
|
public:
|
||
|
|
||
|
void prompt()
|
||
|
{
|
||
|
int sum = sumParen();
|
||
|
bool isBracket = (sum != 0);
|
||
|
|
||
|
sum = sum * tab + norm_prompt.length;
|
||
|
|
||
|
if(isComment)
|
||
|
{
|
||
|
sum -= cmt_prompt.length;
|
||
|
while(sum-->0)
|
||
|
put(" ");
|
||
|
put(cmt_prompt);
|
||
|
}
|
||
|
else if(isBracket)
|
||
|
{
|
||
|
sum -= ml_prompt.length;
|
||
|
while(sum-->0)
|
||
|
put(" ");
|
||
|
put(ml_prompt);
|
||
|
}
|
||
|
else
|
||
|
put(norm_prompt);
|
||
|
}
|
||
|
|
||
|
void addImports(char[][] str...)
|
||
|
{
|
||
|
assert(sc !is null);
|
||
|
sc.registerImport(str);
|
||
|
}
|
||
|
|
||
|
// Get the accumulated output since the last call. Includes
|
||
|
// newlines. Will only work if you have not set an output callback
|
||
|
// function.
|
||
|
char[] output()
|
||
|
{
|
||
|
assert(!hasCallback);
|
||
|
char[] res = outBuf.arrayCopy();
|
||
|
outBuf.length = 0;
|
||
|
return res;
|
||
|
}
|
||
|
|
||
|
// Sets the command prompt (default is "> ")
|
||
|
void setPrompt(char[] prmt)
|
||
|
{ norm_prompt = prmt; }
|
||
|
|
||
|
// Sets the multi-line prompt (default is "... ")
|
||
|
void setMLPrompt(char[] prmt)
|
||
|
{ ml_prompt = prmt; }
|
||
|
|
||
|
// Set tab size (default 4)
|
||
|
void setTabSize(int i)
|
||
|
{ tab = i; }
|
||
|
|
||
|
// Get input. Will sometimes expect multi-line input, for example if
|
||
|
// case a line contains an open brace or an unterminated block
|
||
|
// comment. In that case the console will produce another prompt
|
||
|
// (when you call prompt()), and also return true.
|
||
|
CR input(char[] str)
|
||
|
{
|
||
|
str = str.strip();
|
||
|
if(str == "") return CR.Empty;
|
||
|
|
||
|
try return runInput(str);
|
||
|
catch(MonsterException e)
|
||
|
{
|
||
|
putln(e.toString);
|
||
|
reset();
|
||
|
return CR.Error;
|
||
|
}
|
||
|
}
|
||
|
}
|