66493565cc821

66493565ccdb2
1 Guest is here.
 

66493565cd702RoSoDude

66493565cd75d
I've been messing around with overlay scripting in Squirrel lately; while the NewDark documentation comes with an OSM overlay script sample for SS2, it only provides a Squirrel overlay script sample for Thief 2, which has more limited functionality. So I figured it would be a good exercise to port the OSM overlay script for SS2 as well, and then thought I'd release it for other modders who want to mess around with custom UI so no one else has to go through the pain I did regarding type conversions, constructors, and so on.

I've checked over it a few times and it all works, but there are probably some bits that aren't too efficient (the numpad input stuff is quite messy, as Squirrel treats chars, ints, strings, and arrays rather differently from C++). You'll need to check the rest of the NewDark documentation for how to actually use overlay scripts.

ss2_overlay_sample.nut:
Code: [Select]
// A squirrel.osm port of the C++ SS2 overlay sample ("ss2_overlay_sample.cpp")
// This can be used in place of "ss2sample.osm" with the "Demo_OverlaySampleOsm.mis" mission.
// Adapted by RoSoDude using T2_overlay_sample.nut

//
// Local helper classes for individual HUD elements etc.
//
//

class sRect
{
left = 0;
top = 0;
right = 0;
bottom = 0;

function IsPtInside(x, y) { return x >= left && y >= top && x < right && y < bottom; }
}

// ---------------------------------------------------------------

// base class for our HUD elements
class cHudElement
{
m_bActive = false;
m_rect = null;

constructor()
{
m_rect = sRect();
}

function Toggle() { m_bActive = !m_bActive; }
function Show() { m_bActive = true; OnShow(TRUE); }
function Hide() { m_bActive = false; OnShow(FALSE); }

// required functions in derived classes
function CalcPlacement() {}

function OnShow(bShow) {}

function MouseClick(x, y) { return false; }
function MouseDblClick(x, y) { return false; }
function MouseDragDrop(x, y, start_drag, cursor_mode) { return false; }
function OnKey(ch, keycode) { return false; }

function Draw() {}
}


// a custom HUD element
class cHudElement_Something extends cHudElement
{
m_bgImage = 0;

// keep two temp int_ref objects around which are useful when calling service functions that have integer reference parameters
// (avoids constant object creation and deletion if these were created as temp local vars)
iref1 = int_ref();
iref2 = int_ref();

constructor()
{
base.constructor();

m_bgImage = ShockOverlay.GetBitmap("cyber", "iface\\");

CalcPlacement();
}

function CalcPlacement()
{
m_rect.left = 0;
m_rect.top = 0;

ShockOverlay.GetBitmapSize(m_bgImage, iref1, iref2);
m_rect.right = iref1.tointeger();
m_rect.bottom = iref2.tointeger();

m_rect.right += m_rect.left;
m_rect.bottom += m_rect.top;
}

function Draw()
{
ShockOverlay.DrawBitmap(m_bgImage, m_rect.left, m_rect.top);
}
}

// another custom HUD element
class cHudElement_SomethingElse extends cHudElement
{
m_bgImage = 0;

// keep two temp int_ref objects around which are useful when calling service functions that have integer reference parameters
// (avoids constant object creation and deletion if these were created as temp local vars)
iref1 = int_ref();
iref2 = int_ref();

constructor()
{
base.constructor();

m_bgImage = ShockOverlay.GetBitmap("ammodiag", "iface\\");

CalcPlacement();
}

function CalcPlacement()
{
Engine.GetCanvasSize(iref1, iref2);
local w = iref1.tointeger();
local h = iref2.tointeger();

ShockOverlay.GetBitmapSize(m_bgImage, iref1, iref2);
local bm_w = iref1.tointeger();
local bm_h = iref2.tointeger();

m_rect.left = w-bm_w;
m_rect.top =  0;
m_rect.right = m_rect.left + bm_w;
m_rect.bottom = m_rect.top + bm_h;
}

function Draw()
{
ShockOverlay.DrawBitmap(m_bgImage, m_rect.left, m_rect.top);
ShockOverlay.DrawString("Click me", m_rect.left+20, m_rect.top+12);
}

function MouseClick(x, y)
{
// do something
ShockOverlay.ToggleLookCursor();

return true;
}
}

// takes numeric keyboard input and draws string
// RSD: Many edits here to fix discrepancies between C++ and Squirrel regarding chars, ints, strings, and arrays
class cHudElement_NumInput extends cHudElement
{
static MAX_TEXT = 8;
m_text = array(10); //RSD: MAX_TEXT+2, Squirrel throws error if index MAX_TEXT is used here
m_cursorpos = 0;
textdisp = ""; //RSD: Needed for string input later

// keep two temp int_ref objects around which are useful when calling service functions that have integer reference parameters
// (avoids constant object creation and deletion if these were created as temp local vars)
iref1 = int_ref();
iref2 = int_ref();

/*constructor()
{
base.constructor();
}*/

constructor()
{
base.constructor();

m_text[0] = (0).tochar(); //RSD: Handle char conversion manually
m_cursorpos = 0;

//ShockOverlay.SetCustomFont(0, "bignum", "fonts\\");

CalcPlacement();
}

function CalcPlacement()
{
// stick it somewhere in the middle
Engine.GetCanvasSize(iref1, iref2);
local w = iref1.tointeger();
local h = iref2.tointeger();
m_rect.left = w/2;
m_rect.top =  h/2 + 40;
m_rect.right = m_rect.left + 100;
m_rect.bottom = m_rect.top + 20;
}

function OnShow(bShow)
{
m_text[0] = (0).tochar(); //RSD: Handle char conversion manually
m_cursorpos = 0;

// make sure keypad HUD isn't active
ShockGame.OverlayChange(kOverlayKeypad, kOverlayModeOff);

ShockOverlay.SetKeyboardInputCapture(bShow);
}

function Draw()
{
// draw a blinking underscore at cursor pos
if ((ShockGame.SimTime()/500) & 1)
{
m_text[m_cursorpos] = "_";
m_text[m_cursorpos+1] = (0).tochar(); //RSD: Handle char conversion manually
}

ShockOverlay.SetTextColor(255, 255, 0);
//ShockOverlay.SetFont(2);
ShockOverlay.SetFont(1);
textdisp = m_text.reduce(function(p,c) {
if (c != null)
return p.tostring() + c.tostring();
else
return p.tostring();
}); //RSD: Squirrel reads array pointer if used directly as input string
ShockOverlay.DrawString(textdisp, m_rect.left, m_rect.top);
ShockOverlay.SetFont(0);
ShockOverlay.SetTextColor(-1, 0, 0);

m_text[m_cursorpos] = (0).tochar(); //RSD: Handle char conversion manually
}

function OnKey(ch, keycode)
{
// process number key from main keyboard and numberic key pad both when numlock is on and off
switch(keycode) //RSD:  Squirrel can't work with chars as ints (nor will it compare strings bitwise with ints)
{
case ('0').tointeger():
case ('0').tointeger()|KB_FLAG_2ND:
case KEY_PAD_INS: ProcessKey(0); break;
case ('1').tointeger():
case ('1').tointeger()|KB_FLAG_2ND:
case KEY_PAD_END: ProcessKey(1); break;
case ('2').tointeger():
case ('2').tointeger()|KB_FLAG_2ND:
case KEY_PAD_DOWN: ProcessKey(2); break;
case ('3').tointeger():
case ('3').tointeger()|KB_FLAG_2ND:
case KEY_PAD_PGDN: ProcessKey(3); break;
case ('4').tointeger():
case ('4').tointeger()|KB_FLAG_2ND:
case KEY_PAD_LEFT: ProcessKey(4); break;
case ('5').tointeger():
case ('5').tointeger()|KB_FLAG_2ND:
case KEY_PAD_CENTER: ProcessKey(5); break;
case ('6').tointeger():
case ('6').tointeger()|KB_FLAG_2ND:
case KEY_PAD_RIGHT: ProcessKey(6); break;
case ('7').tointeger():
case ('7').tointeger()|KB_FLAG_2ND:
case KEY_PAD_HOME: ProcessKey(7); break;
case ('8').tointeger():
case ('8').tointeger()|KB_FLAG_2ND:
case KEY_PAD_UP: ProcessKey(8); break;
case ('9').tointeger():
case ('9').tointeger()|KB_FLAG_2ND:
case KEY_PAD_PGUP: ProcessKey(9); break;

case KEY_BS:
case KEY_DEL:
case KEY_PAD_DEL: ProcessKey(-1); break;

case KEY_ENTER:
case KEY_GREY_ENTER:
// dactivate HUD element on return/enter
Hide();
break;

default:
// unhandled key
return false;
}

return true;
}

function ProcessKey(num)
{
if (num == -1)
{
// delete
if (m_cursorpos > 0)
{
m_cursorpos--;
m_text[m_cursorpos] = (0).tochar(); //RSD: Handle char conversion manually
}
return;
}

if (m_cursorpos < MAX_TEXT)
{
m_text[m_cursorpos] = (('0').tointeger() + num).tochar(); //RSD: Handle char conversion manually
m_cursorpos++;
m_text[m_cursorpos] = (0).tochar(); //RSD: Handle char conversion manually
}
}
};

// ---------------------------------------------------------------

// base class for our transparent (non-interactive) overlay elements
class cOverlayElement
{
m_bActive = false;
m_handle = -1;

constructor()
{
}

function Toggle() { m_bActive = !m_bActive; }
function Show() { m_bActive = true; }
function Hide() { m_bActive = false; }

// required functions in derived classes
function CalcPlacement() {}

function Draw() {}
}


// a custom overlay element
class cOverlayElement_Something extends cOverlayElement
{
// keep four temp int_ref objects around which are useful when calling service functions that have integer reference parameters
// (avoids constant object creation and deletion if these were created as temp local vars)
iref1 = int_ref();
iref2 = int_ref();
iref3 = int_ref();
iref4 = int_ref();

constructor()
{
base.constructor();

// create a simple static overlay of a bitmap

CalcPos(iref1,iref2);
local x = iref1.tointeger();
local y = iref2.tointeger();

local bm = ShockOverlay.GetBitmap("hackicon", "iface\\");

m_handle = ShockOverlay.CreateTOverlayItemFromBitmap(x, y, 127, bm, false);
}

function CalcPos(xref, yref)
{
// place it where the alarm icon would normal be shown
ShockOverlay.GetOverlayRect(kOverlayAlarm, xref, yref, iref3, iref4);
}

function CalcPlacement()
{
CalcPos(iref1,iref2);
local x = iref1.tointeger();
local y = iref2.tointeger();
ShockOverlay.UpdateTOverlayPosition(m_handle, x, y);
}

function Draw()
{
ShockOverlay.DrawTOverlayItem(m_handle);
}
}

// another custom overlay element
class cOverlayElement_SomethingElse extends cOverlayElement
{
m_bUpdateContents = false;
m_bgImage = 0;
x = 0;
y = 0;

// keep four temp int_ref objects around which are useful when calling service functions that have integer reference parameters
// (avoids constant object creation and deletion if these were created as temp local vars)
iref1 = int_ref();
iref2 = int_ref();
iref3 = int_ref();
iref4 = int_ref();

constructor()
{
base.constructor();

// create a dynamic/comlpex overlay with 64x64 size

CalcPos();

m_handle = ShockOverlay.CreateTOverlayItem(x, y, 64, 64, 127, false);
m_bUpdateContents = true;

// get our bg bitmap
m_bgImage = ShockOverlay.GetBitmap("alarm", "iface\\");
}

function CalcPos()
{
// place it right of where the alarm icon would normal be shown
ShockOverlay.GetOverlayRect(kOverlayAlarm, iref1, iref2, iref3, iref4);
x = iref1.tointeger();
y = iref2.tointeger();
local w = iref3.tointeger();
x += w + 4;
// xref = x.int_ref();
}

function CalcPlacement()
{
CalcPos();

ShockOverlay.UpdateTOverlayPosition(m_handle, x, y);
}

function Draw()
{
// draw overlay contents to update it if something changed
if (m_bUpdateContents)
{
m_bUpdateContents = false;

if ( ShockOverlay.BeginTOverlayUpdate(m_handle) )
{
local s = ShockGame.GetPlayerPsiPoints();

ShockOverlay.GetStringSize(s, iref1, iref2);
local w = iref1.tointeger();
local h = iref2.tointeger();

ShockOverlay.DrawBitmap(m_bgImage, 0, 0);
ShockOverlay.DrawString(s, 64-w-4, 64-16);

ShockOverlay.EndTOverlayUpdate();
}
}

ShockOverlay.DrawTOverlayItem(m_handle);
}
}


/****************************************************************************/


//
// The overlay handler interface
// Receives calls from the engine. Only one handler (per OSM) can be active at a time.
//

class cMyShockOverlay extends IShockOverlayHandler
{
m_elems = [];
m_overlays = [];

/*constructor()
{
base.constructor();
}*/

function FindElementFromPt(x, y)
{
foreach (o in m_elems)
if (o.m_bActive && o.m_rect.IsPtInside(x,y))
return o;

return null;
}

function Init()
{
m_elems = [];
m_elems.append( cHudElement_Something() );
m_elems.append( cHudElement_SomethingElse() );
m_elems.append( cHudElement_NumInput() );

m_overlays = [];
m_overlays.append( cOverlayElement_Something() );
m_overlays.append( cOverlayElement_SomethingElse() );

// show em all

foreach (o in m_elems)
o.Show();

foreach (o in m_overlays)
o.Show();
}

function Term()
{
foreach (i, o in m_elems)
m_elems[i] = null;

foreach (i, o in m_overlays)
m_overlays[i] = null;
}

//
// IShockOverlayHandler interface
//

function DrawHUD()
{
foreach (o in m_elems)
if (o.m_bActive)
o.Draw();
}

function DrawTOverlay()
{
foreach (o in m_overlays)
if (o.m_bActive)
o.Draw();
}

function OnUIEnterMode()
{
foreach (o in m_elems)
o.CalcPlacement();

foreach (o in m_overlays)
o.CalcPlacement();
}

function CanEnableElement(which)
{
// prevent the alarm icon from being displayed
if (which == kOverlayAlarm)
return false;
// deactivate numeric input HUD elem if keypad is about to be shown
else if (which == kOverlayKeypad)
m_elems[2].Hide();

return true;
}

function IsMouseOver(x, y)
{
// see if mouse cursor is inside any of our custom elements
return FindElementFromPt(x, y) != null;
}

function MouseClick(x, y)
{
local pElem = FindElementFromPt(x, y);
if (pElem)
return pElem.MouseClick(x, y);

return false;
}

function MouseDblClick(x, y)
{
local pElem = FindElementFromPt(x, y);
if (pElem)
return pElem.MouseDblClick(x, y);

return false;
}

function MouseDragDrop(x, y, start_drag, cursor_mode)
{
local pElem = FindElementFromPt(x, y);
if (pElem)
return pElem.MouseDragDrop(x, y, start_drag, cursor_mode);

return false;
}

function HandleKey(ch, keycode)
{
if (keycode & KB_FLAG_DOWN)
{
foreach (o in m_elems)
if ( o.OnKey(ch, keycode&~KB_FLAG_DOWN) )
return true;
}

return false;
}
}

// create a global instance of the overlay handler
myOverlay <- cMyShockOverlay();

// ================================================================================
// Script that installs and uninstalls the overlay handler
class MyHudScript extends SqRootScript
{
function destructor()
{
// to be on the safe side make really sure the handler is removed when this script is destroyed
// (calling RemoveHandler if it's already been removed is no problem)
ShockOverlay.RemoveHandler(myOverlay);
}

function OnBeginScript()
{
ShockOverlay.AddHandler(myOverlay);
myOverlay.Init();
}

function OnEndScript()
{
ShockOverlay.RemoveHandler(myOverlay);
myOverlay.Term();
}
}

Includes the following sample functionality:
-HUD picture displays (security alarm icons)
-MFD picture displays (big box in the upper left)
-MFD buttons (click me box in upper right, activates the "?" functionality)
-Numpad entry (displays on HUD with blinking "_")
« Last Edit: 10. April 2021, 16:52:29 by RoSoDude »

66493565cd8b1ZylonBane

66493565cd8fd
If you only need to display a single overlay, that entire framework can be dispensed with and the code becomes much simpler, as in the HUD Logs mod.

66493565cdbebRoSoDude

66493565cdc3a
Some other notes:

  • Native overlay elements always display on top of custom overlay elements. Additionally, mouse events are intercepted by native overlays first, so you can't use any custom functions to read or block mouse input when the user is hovering/clicking a native element.
  • The width and height of native overlay elements you can get from GetOverlayRect() doesn't always seem right. Use GetBitmapSize() on the background image instead if this comes up.
  • The DrawHUD() and DrawTOverlay() overlay handler functions run continuously, which is used to update any custom element properties/displays in their own Draw() functions.
  • Avoid any scenario where a custom HUD element gets hidden during its own mouse click function, as it will crash the MFD window. Why? Because the overlay handler has to query each custom HUD element for mouse click behavior, so if the HUD element gets hidden while it's still inside the mouse click function, the overlay handler doesn't know what to do and closes the MFD. This is particularly relevant if you're activating a native overlay inside the mouse click function and then using CanEnableElement(which) in the overlay handler to hide the custom HUD element when which == [the native overlay]. Instead, set bActivate = true inside the mouse click function, and then bung any overlay actions into the element's Draw() function, checking for bActivate and then setting it to false right away (so you don't get an infinite loop from one mouse click).
  • Because they don't inherit from SqRootScript, custom HUD element classes can't use the SendMessage() or PostMessage() functions. If you want to send messages to objects, you'll have to find some other way to hack it, like creating a message hacker archetype with a custom script to sends a message based on its properties and then deletes itself; maybe there are parallel solutions that others have found, that's just what I used.
« Last Edit: 06. April 2021, 20:13:20 by RoSoDude »

66493565ce03aZylonBane

66493565ce08d
The DrawHUD() and DrawTOverlay() overlay handler functions run continuously...
This right here stressed me out when I first started messing with the overlay functions, because the idea of invoking an interpreted script engine 60 times per second (or more) seemed guaranteed to impact the frame rate. But even on my years-old rig, it didn't slow things down at all. I guess Squirrel is optimized well enough, and CPUs have gotten fast enough, that even non-native code can handle these sort of things. Though it probably helps that the Dark Engine doesn't exactly stress modern CPUs.
1 Guest is here.
Don't step on corn! Flag the corn.
Contact SMF 2.0.19 | SMF © 2016, Simple Machines | Terms and Policies
FEEP
66493565ce169