1
0
forked from 0ad/0ad
0ad/source/ps/CConsole.cpp
Ykkrosh 7dca91f26b # Various changes to the text rendering system.
Rewrite font builder tool to be much simpler and to support more text
effects.
Change GUI to use new set of fonts.
Switch font textures from TGA to PNG so they're easier for the font
builder to create.
Support RGBA font textures (for e.g. stroked text).
Greatly improve text rendering performance by using vertex arrays.
Fix rendering code leaving vertex buffers bound.
Add 'clip' property to GUI text objects, to disable clipping when
rendering.
Delete part of unused console function registration system.

This was SVN commit r7595.
2010-05-30 13:42:56 +00:00

812 lines
18 KiB
C++

/* Copyright (C) 2010 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. 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
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
*/
/*
* Implements the in-game console with scripting support.
*/
#include "precompiled.h"
#include <wctype.h>
#include "CConsole.h"
#include "lib/ogl.h"
#include "lib/res/graphics/unifont.h"
#include "lib/sysdep/clipboard.h"
#include "maths/MathUtil.h"
#include "network/NetClient.h"
#include "network/NetServer.h"
#include "ps/CLogger.h"
#include "ps/Filesystem.h"
#include "ps/Globals.h"
#include "ps/Hotkey.h"
#include "ps/Pyrogenesis.h"
#include "scripting/ScriptingHost.h"
#include "scripting/ScriptableComplex.inl"
#define LOG_CATEGORY L"Console"
CConsole* g_Console = 0;
CConsole::CConsole()
{
m_bToggle = false;
m_bVisible = false;
m_fVisibleFrac = 0.0f;
m_szBuffer = new wchar_t[CONSOLE_BUFFER_SIZE];
FlushBuffer();
m_iMsgHistPos = 1;
m_charsPerPage=0;
InsertMessage(L"[ 0 A.D. Console v0.12 ] type \"\\info\" for help");
InsertMessage(L"");
if (FileExists(L"gui/text/help.txt"))
{
shared_ptr<u8> buf; size_t size;
if ( g_VFS->LoadFile(L"gui/text/help.txt", buf, size) < 0 )
{
LOG(CLogger::Error, LOG_CATEGORY, L"Help file not found for console");
return;
}
// TODO: read in text mode, or at least get rid of the \r\n somehow
// TODO: maybe the help file should be UTF-8 - we assume it's iso-8859-1 here
m_helpText = CStrW((const char*)buf.get());
}
else
{
InsertMessage(L"No help file found.");
}
}
CConsole::~CConsole()
{
m_deqMsgHistory.clear();
m_deqBufHistory.clear();
delete[] m_szBuffer;
}
void CConsole::SetSize(float X, float Y, float W, float H)
{
m_fX = X;
m_fY = Y;
m_fWidth = W;
m_fHeight = H;
}
void CConsole::UpdateScreenSize(int w, int h)
{
float height = h * 0.6f;
SetSize(0, h-height, (float)w, height);
}
void CConsole::ToggleVisible()
{
m_bToggle = true;
m_bVisible = !m_bVisible;
}
void CConsole::SetVisible( bool visible )
{
if( visible != m_bVisible )
m_bToggle = true;
m_bVisible = visible;
}
void CConsole::FlushBuffer(void)
{
// Clear the buffer and set the cursor and length to 0
memset(m_szBuffer, '\0', sizeof(wchar_t) * CONSOLE_BUFFER_SIZE);
m_iBufferPos = m_iBufferLength = 0;
}
void CConsole::ToLower(wchar_t* szMessage, size_t iSize)
{
size_t L = (size_t)wcslen(szMessage);
if (L <= 0) return;
if (iSize && iSize < L) L = iSize;
for(size_t i = 0; i < L; i++)
szMessage[i] = towlower(szMessage[i]);
}
void CConsole::Trim(wchar_t* szMessage, const wchar_t cChar, size_t iSize)
{
size_t L = wcslen(szMessage);
if(!L)
return;
if (iSize && iSize < L) L = iSize;
wchar_t szChar[2] = { cChar, 0 };
// Find the first point at which szChar does not
// exist in the message
size_t ofs = wcsspn(szMessage, szChar);
if(ofs == 0) // no leading <cChar> chars - we're done
return;
// move everything <ofs> chars left, replacing leading cChar chars
L -= ofs;
memmove(szMessage, szMessage+ofs, L*sizeof(wchar_t));
for(ssize_t i = (ssize_t)L; i >= 0; i--)
{
szMessage[i] = '\0';
if (szMessage[i - 1] != cChar) break;
}
}
void CConsole::Update(const float DeltaTime)
{
if(m_bToggle)
{
const float AnimateTime = .30f;
const float Delta = DeltaTime / AnimateTime;
if(m_bVisible)
{
m_fVisibleFrac += Delta;
if(m_fVisibleFrac > 1.0f)
{
m_fVisibleFrac = 1.0f;
m_bToggle = false;
}
}
else
{
m_fVisibleFrac -= Delta;
if(m_fVisibleFrac < 0.0f)
{
m_fVisibleFrac = 0.0f;
m_bToggle = false;
}
}
}
}
//Render Manager.
void CConsole::Render()
{
if (! (m_bVisible || m_bToggle) ) return;
// animation: slide in from top of screen
const float MaxY = m_fHeight;
const float DeltaY = (1.0f - m_fVisibleFrac) * MaxY;
glPushMatrix();
glTranslatef(m_fX, m_fY + DeltaY, 0.0f); //Move to window position
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
DrawWindow();
DrawHistory();
DrawBuffer();
glDisable(GL_BLEND);
glPopMatrix();
}
void CConsole::DrawWindow(void)
{
// TODO: Add texturing
glDisable(GL_TEXTURE_2D);
// Draw Background
// Set the color to a translucent blue
glColor4f(0.0f, 0.0f, 0.5f, 0.6f);
glBegin(GL_QUADS);
glVertex2f(0.0f, 0.0f);
glVertex2f(m_fWidth-1.0f, 0.0f);
glVertex2f(m_fWidth-1.0f, m_fHeight-1.0f);
glVertex2f(0.0f, m_fHeight-1.0f);
glEnd();
// Draw Border
// Set the color to a translucent yellow
glColor4f(0.5f, 0.5f, 0.0f, 0.6f);
glBegin(GL_LINE_LOOP);
glVertex2f(0.0f, 0.0f);
glVertex2f(m_fWidth-1.0f, 0.0f);
glVertex2f(m_fWidth-1.0f, m_fHeight-1.0f);
glVertex2f(0.0f, m_fHeight-1.0f);
glEnd();
if (m_fHeight > m_iFontHeight + 4)
{
glBegin(GL_LINES);
glVertex2f(0.0f, (GLfloat)(m_iFontHeight + 4));
glVertex2f(m_fWidth, (GLfloat)(m_iFontHeight + 4));
glEnd();
}
glEnable(GL_TEXTURE_2D);
}
void CConsole::DrawHistory(void) {
int i = 1;
std::deque<std::wstring>::iterator Iter; //History iterator
glPushMatrix();
glColor3f(1.0f, 1.0f, 1.0f); //Set color of text
glTranslatef(9.0f, (float)m_iFontOffset, 0.0f); //move away from the border
// Draw the text upside-down, because it's aligned with
// the GUI (which uses the top-left as (0,0))
glScalef(1.0f, -1.0f, 1.0f);
for (Iter = m_deqMsgHistory.begin();
Iter != m_deqMsgHistory.end()
&& (((i - m_iMsgHistPos + 1) * m_iFontHeight) < m_fHeight);
Iter++)
{
if (i >= m_iMsgHistPos){
glTranslatef(0.0f, -(float)m_iFontHeight, 0.0f);
glPushMatrix();
glwprintf(L"%ls", Iter->c_str());
glPopMatrix();
}
i++;
}
glPopMatrix();
}
//Renders the buffer to the screen.
void CConsole::DrawBuffer(void)
{
if (m_fHeight < m_iFontHeight) return;
glPushMatrix();
glColor3f(1.0f, 1.0f, 0.0f);
glTranslatef(2.0f, (float)m_iFontOffset, 0);
glScalef(1.0f, -1.0f, 1.0f);
glwprintf(L"]");
glColor3f(1.0f, 1.0f, 1.0f);
if (m_iBufferPos==0) DrawCursor();
for (int i = 0; i < m_iBufferLength; i++){
glwprintf(L"%lc", m_szBuffer[i]);
if (m_iBufferPos-1==i) DrawCursor();
}
glPopMatrix();
}
void CConsole::DrawCursor(void)
{
// (glPushMatrix is necessary because glwprintf does glTranslatef)
glPushMatrix();
// Slightly translucent yellow
glColor4f(1.0f, 1.0f, 0.0f, 0.8f);
// Cursor character is chosen to be an underscore
glwprintf(L"_");
// Revert to the standard text colour
glColor3f(1.0f, 1.0f, 1.0f);
glPopMatrix();
}
//Inserts a character into the buffer.
void CConsole::InsertChar(const int szChar, const wchar_t cooked )
{
static int iHistoryPos = -1;
if (!m_bVisible) return;
switch (szChar){
case SDLK_RETURN:
iHistoryPos = -1;
m_iMsgHistPos = 1;
ProcessBuffer(m_szBuffer);
FlushBuffer();
return;
case SDLK_TAB:
// Auto Complete
return;
case SDLK_BACKSPACE:
if (IsEmpty() || IsBOB()) return;
if (m_iBufferPos == m_iBufferLength)
m_szBuffer[m_iBufferPos - 1] = '\0';
else{
for(int j=m_iBufferPos-1; j<m_iBufferLength-1; j++)
m_szBuffer[j] = m_szBuffer[j+1]; // move chars to left
m_szBuffer[m_iBufferLength-1] = '\0';
}
m_iBufferPos--;
m_iBufferLength--;
return;
case SDLK_DELETE:
if (IsEmpty() || IsEOB()) return;
if (m_iBufferPos == m_iBufferLength-1)
{
m_szBuffer[m_iBufferPos] = '\0';
m_iBufferLength--;
}
else
{
if (g_keys[SDLK_RCTRL] || g_keys[SDLK_LCTRL])
{
// Make Ctrl-Delete delete up to end of line
m_szBuffer[m_iBufferPos] = '\0';
m_iBufferLength = m_iBufferPos;
}
else
{
// Delete just one char and move the others left
for(int j=m_iBufferPos; j<m_iBufferLength-1; j++)
m_szBuffer[j] = m_szBuffer[j+1];
m_szBuffer[m_iBufferLength-1] = '\0';
m_iBufferLength--;
}
}
return;
case SDLK_HOME:
if (g_keys[SDLK_RCTRL] || g_keys[SDLK_LCTRL])
{
int linesShown = (int)m_fHeight/m_iFontHeight - 4;
m_iMsgHistPos = clamp((int)m_deqMsgHistory.size() - linesShown, 1, (int)m_deqMsgHistory.size());
}
else
{
m_iBufferPos = 0;
}
return;
case SDLK_END:
if (g_keys[SDLK_RCTRL] || g_keys[SDLK_LCTRL])
{
m_iMsgHistPos = 1;
}
else
{
m_iBufferPos = m_iBufferLength;
}
return;
case SDLK_LEFT:
if (m_iBufferPos) m_iBufferPos--;
return;
case SDLK_RIGHT:
if (m_iBufferPos != m_iBufferLength) m_iBufferPos++;
return;
// BEGIN: Buffer History Lookup
case SDLK_UP:
if ( m_deqBufHistory.size() )
{
int oldHistoryPos = iHistoryPos;
while( iHistoryPos != (int)m_deqBufHistory.size() - 1)
{
iHistoryPos++;
std::wstring& histString = m_deqBufHistory.at(iHistoryPos);
if((int)histString.length() >= m_iBufferPos)
{
bool bad = false;
for(int i=0; i<m_iBufferPos; i++)
{
if(histString[i] != m_szBuffer[i])
{
bad = true; break;
}
}
if(!bad)
{
SetBuffer(m_deqBufHistory.at(iHistoryPos).c_str());
return;
}
}
}
// if we got here, failed to find a string with the right prefix;
// revert to the old position in case the user types more stuff
iHistoryPos = oldHistoryPos;
}
return;
case SDLK_DOWN:
if ( m_deqBufHistory.size() && iHistoryPos > 0 )
{
int oldHistoryPos = iHistoryPos;
while( iHistoryPos != 0)
{
iHistoryPos--;
std::wstring& histString = m_deqBufHistory.at(iHistoryPos);
if((int)histString.length() >= m_iBufferPos)
{
bool bad = false;
for(int i=0; i<m_iBufferPos; i++)
{
if(histString[i] != m_szBuffer[i])
{
bad = true; break;
}
}
if(!bad)
{
SetBuffer(m_deqBufHistory.at(iHistoryPos).c_str());
return;
}
}
}
// if we got here, failed to find a string with the right prefix;
// revert to the old position in case the user types more stuff,
// and also clear any complietion we might've added going up
iHistoryPos = oldHistoryPos;
m_szBuffer[m_iBufferPos] = 0;
m_iBufferLength = m_iBufferPos;
}
return;
// END: Buffer History Lookup
// BEGIN: Message History Lookup
case SDLK_PAGEUP:
if (m_iMsgHistPos != (int)m_deqMsgHistory.size()) m_iMsgHistPos++;
return;
case SDLK_PAGEDOWN:
if (m_iMsgHistPos != 1) m_iMsgHistPos--;
return;
// END: Message History Lookup
default: //Insert a character
if (IsFull()) return;
if (cooked == 0) return;
if (IsEOB()) //are we at the end of the buffer?
m_szBuffer[m_iBufferPos] = cooked; //cat char onto end
else{ //we need to insert
int i;
for(i=m_iBufferLength; i>m_iBufferPos; i--)
m_szBuffer[i] = m_szBuffer[i-1]; // move chars to right
m_szBuffer[i] = cooked;
}
m_iBufferPos++;
m_iBufferLength++;
return;
}
}
void CConsole::InsertMessage(const wchar_t* szMessage, ...)
{
va_list args;
wchar_t szBuffer[CONSOLE_MESSAGE_SIZE];
va_start(args, szMessage);
if (vswprintf(szBuffer, CONSOLE_MESSAGE_SIZE, szMessage, args) == -1)
{
debug_printf(L"Error printfing console message (buffer size exceeded?)\n");
// Make it obvious that the text was trimmed (assuming it was)
wcscpy(szBuffer+CONSOLE_MESSAGE_SIZE-4, L"...");
}
va_end(args);
InsertMessageRaw(szBuffer);
}
void CConsole::InsertMessageRaw(const CStrW& message)
{
// (TODO: this text-wrapping is rubbish since we now use variable-width fonts)
//Insert newlines to wraparound text where needed
CStrW wrapAround(message);
CStrW newline(L'\n');
size_t oldNewline=0;
size_t distance;
//make sure everything has been initialized
if ( m_charsPerPage != 0 )
{
while ( oldNewline+m_charsPerPage < wrapAround.length() )
{
distance = wrapAround.find(newline, oldNewline) - oldNewline;
if ( distance > m_charsPerPage )
{
oldNewline += m_charsPerPage;
wrapAround.insert( oldNewline++, newline );
}
else
oldNewline += distance+1;
}
}
// Split into lines and add each one individually
oldNewline = 0;
while ( (distance = wrapAround.find(newline, oldNewline)) != wrapAround.npos)
{
distance -= oldNewline;
m_deqMsgHistory.push_front(wrapAround.substr(oldNewline, distance));
oldNewline += distance+1;
}
m_deqMsgHistory.push_front(wrapAround.substr(oldNewline));
}
const wchar_t* CConsole::GetBuffer()
{
m_szBuffer[m_iBufferLength] = 0;
return( m_szBuffer );
}
void CConsole::SetBuffer(const wchar_t* szMessage)
{
int oldBufferPos = m_iBufferPos; // remember since FlushBuffer will set it to 0
FlushBuffer();
wcsncpy(m_szBuffer, szMessage, CONSOLE_BUFFER_SIZE);
m_iBufferLength = (int)wcslen(m_szBuffer);
m_iBufferPos = std::min(oldBufferPos, m_iBufferLength);
}
void CConsole::UseHistoryFile(const VfsPath& filename, int max_history_lines)
{
m_MaxHistoryLines = max_history_lines;
m_sHistoryFile = filename;
LoadHistory();
}
void CConsole::ProcessBuffer(const wchar_t* szLine)
{
if (szLine == NULL) return;
if (wcslen(szLine) <= 0) return;
debug_assert(wcslen(szLine) < CONSOLE_BUFFER_SIZE);
m_deqBufHistory.push_front(szLine);
SaveHistory(); // Do this each line for the moment; if a script causes
// a crash it's a useful record.
wchar_t szCommand[CONSOLE_BUFFER_SIZE] = { 0 };
if (szLine[0] == '\\')
{
if (swscanf(szLine, L"\\%ls", szCommand) != 1)
return;
Trim(szCommand);
ToLower(szCommand);
if (!wcscmp(szCommand, L"info"))
{
InsertMessage(L"");
InsertMessage(L"[Information]");
InsertMessage(L" -View commands \"\\commands\"");
InsertMessage(L" -Call command \"\\<command>\"");
InsertMessage(L" -Say \"<string>\"");
InsertMessage(L" -Help - Lists functions usable from console");
InsertMessage(L"");
}
else if (!wcscmp(szCommand, L"commands"))
{
InsertMessage(L"");
InsertMessage(L"[Commands]");
InsertMessage(L" (none registered)");
InsertMessage(L"");
}
else if (! (wcscmp(szCommand, L"Help") && wcscmp(szCommand, L"help")) )
{
InsertMessage(L"");
InsertMessage(L"[Help]");
InsertMessageRaw(m_helpText);
}
else
{
InsertMessage(L"unknown command <%ls>", szCommand);
}
}
else if (szLine[0] == ':' || szLine[0] == '?')
{
// Process it as JavaScript
jsval rval = g_ScriptingHost.ExecuteScript( szLine+1, L"Console" );
if (szLine[0] == '?' && rval)
{
try {
InsertMessage( L"%ls", g_ScriptingHost.ValueToUCString( rval ).c_str() );
} catch (PSERROR_Scripting_ConversionFailed) {
InsertMessage( L"%hs", "<error converting return value to string>" );
}
}
}
else
{
SendChatMessage(szLine);
}
}
void CConsole::LoadHistory()
{
// note: we don't care if this file doesn't exist or can't be read;
// just don't load anything in that case.
// do this before LoadFile to avoid an error message if file not found.
if (!FileExists(m_sHistoryFile))
return;
shared_ptr<u8> buf; size_t buflen;
if (g_VFS->LoadFile(m_sHistoryFile, buf, buflen) < 0)
return;
CStr bytes ((char*)buf.get(), buflen);
CStrW str (bytes.FromUTF8());
size_t pos = 0;
while (pos != CStrW::npos)
{
pos = str.find('\n');
if (pos != CStrW::npos)
{
if (pos > 0)
m_deqBufHistory.push_front(str.Left(str[pos-1] == '\r' ? pos - 1 : pos));
str = str.substr(pos + 1);
}
else if (str.length() > 0)
m_deqBufHistory.push_front(str);
}
}
void CConsole::SaveHistory()
{
WriteBuffer buffer;
const int linesToSkip = (int)m_deqBufHistory.size() - m_MaxHistoryLines;
std::deque<std::wstring>::reverse_iterator it = m_deqBufHistory.rbegin();
if(linesToSkip > 0)
std::advance(it, linesToSkip);
for (; it != m_deqBufHistory.rend(); ++it)
{
CStr8 line = CStrW(*it).ToUTF8();
buffer.Append(line.data(), line.length());
static const char newline = '\n';
buffer.Append(&newline, 1);
}
g_VFS->CreateFile(m_sHistoryFile, buffer.Data(), buffer.Size());
}
void CConsole::SendChatMessage(const wchar_t *pText)
{
CNetHost *pHost = NULL;
if ( pHost )
{
CChatMessage chat;
chat.m_Recipient = CHAT_RECIPIENT_ALL;
chat.m_Message = pText;
if ( pHost->IsServer() )
{
CNetServer* pServer = ( CNetServer* )pHost;
chat.m_Sender = 0;
ReceivedChatMessage( pServer->GetPlayerName(), chat.m_Message.c_str() );
}
pHost->Broadcast( &chat );
}
}
void CConsole::ReceivedChatMessage(const wchar_t *szSender, const wchar_t *szMessage)
{
InsertMessage(L"%ls: %ls", szSender, szMessage);
}
static bool isUnprintableChar(SDL_keysym key)
{
// U+0000 to U+001F are control characters
if (key.unicode < 0x20)
{
switch (key.sym)
{
// We want to allow some, which are handled specially
case SDLK_RETURN: case SDLK_TAB:
case SDLK_BACKSPACE: case SDLK_DELETE:
case SDLK_HOME: case SDLK_END:
case SDLK_LEFT: case SDLK_RIGHT:
case SDLK_UP: case SDLK_DOWN:
case SDLK_PAGEUP: case SDLK_PAGEDOWN:
return false;
// Ignore the others
default:
return true;
}
}
return false;
}
InReaction conInputHandler(const SDL_Event_* ev)
{
if( ev->ev.type == SDL_HOTKEYDOWN )
{
if( ev->ev.user.code == HOTKEY_CONSOLE_TOGGLE )
{
g_Console->ToggleVisible();
return IN_HANDLED;
}
else if( ev->ev.user.code == HOTKEY_CONSOLE_COPY )
{
sys_clipboard_set( g_Console->GetBuffer() );
return IN_HANDLED;
}
else if( ev->ev.user.code == HOTKEY_CONSOLE_PASTE )
{
wchar_t* text = sys_clipboard_get();
if(text)
{
for(wchar_t* c = text; *c; c++)
g_Console->InsertChar(0, *c);
sys_clipboard_free(text);
}
return IN_HANDLED;
}
}
if( ev->ev.type != SDL_KEYDOWN)
return IN_PASS;
SDLKey sym = ev->ev.key.keysym.sym;
if(!g_Console->IsActive())
return IN_PASS;
// Stop unprintable characters (ctrl+, alt+ and escape),
// also prevent ` and/or ~ appearing in console every time it's toggled.
if( !isUnprintableChar(ev->ev.key.keysym) &&
!hotkeys[HOTKEY_CONSOLE_TOGGLE] )
g_Console->InsertChar(sym, (wchar_t)ev->ev.key.keysym.unicode );
return IN_PASS;
}