DEADSOFTWARE

gl: move holmes drawing code into render
[d2df-sdl.git] / src / game / g_holmes.pas
index 007ffbb0be37380b2a7087e8fe1929a08b6aefca..b7ad74b79db8028d84f2711100434655a9e5eebe 100644 (file)
@@ -1,9 +1,8 @@
-(* Copyright (C)  DooM 2D:Forever Developers
+(* Copyright (C)  Doom 2D: Forever Developers
  *
  * This program 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 3 of the License, or
- * (at your option) any later version.
+ * the Free Software Foundation, version 3 of the License ONLY.
  *
  * This program is distributed in the hope that it will be useful,
  * but WITHOUT ANY WARRANTY; without even the implied warranty of
@@ -18,377 +17,909 @@ unit g_holmes;
 
 interface
 
-uses
-  e_log,
-  g_textures, g_basic, e_graphics, g_phys, g_grid, g_player, g_monsters,
-  g_window, g_map, g_triggers, g_items, g_game, g_panel, g_console,
-  xprofiler;
-
-
-type
-  THMouseEvent = record
-  public
-    const
-      // both for but and for bstate
-      Left = $0001;
-      Right = $0002;
-      Middle = $0004;
-      WheelUp = $0008;
-      WheelDown = $0010;
-
-      // event types
-      Motion = 0;
-      Press = 1;
-      Release = 2;
-
-  public
-    kind: Byte; // motion, press, release
-    x, y: Integer;
-    dx, dy: Integer; // for wheel this is wheel motion, otherwise this is relative mouse motion
-    but: Word; // current pressed button or 0
-    bstate: Word; // button state
-    kstate: Word; // keyboard state (see THKeyEvent);
-  end;
-
-  THKeyEvent = record
-  public
-    const
-      ModCtrl = $0001;
-      ModAlt = $0002;
-      ModShift = $0004;
-  end;
+  procedure holmesInitCommands ();
+  procedure holmesInitBinds ();
 
+  function monsTypeToString (mt: Byte): AnsiString;
+  function monsBehToString (bt: Byte): AnsiString;
+  function monsStateToString (st: Byte): AnsiString;
+  function trigType2Str (ttype: Integer): AnsiString;
 
-procedure g_Holmes_VidModeChanged ();
-procedure g_Holmes_WindowFocused ();
-procedure g_Holmes_WindowBlured ();
-
-procedure g_Holmes_Draw ();
+  var
+    g_holmes_imfunctional: Boolean = false;
+    g_holmes_enabled: Boolean = {$IF DEFINED(D2F_DEBUG)}true{$ELSE}false{$ENDIF};
 
-function g_Holmes_mouseEvent (var ev: THMouseEvent): Boolean; // returns `true` if event was eaten
-function g_Holmes_KeyEvent (var ev: THKeyEvent): Boolean; // returns `true` if event was eaten
+  var
+    msX: Integer = -666;
+    msY: Integer = -666;
+    showGrid: Boolean = {$IF DEFINED(D2F_DEBUG)}false{$ELSE}false{$ENDIF};
+    showMonsInfo: Boolean = false;
+    showMonsLOS2Plr: Boolean = false;
+    showAllMonsCells: Boolean = false;
+    showMapCurPos: Boolean = false;
+    showLayersWindow: Boolean = false;
+    showOutlineWindow: Boolean = false;
+    showTriggers: Boolean = {$IF DEFINED(D2F_DEBUG)}false{$ELSE}false{$ENDIF};
+    showTraceBox: Boolean = {$IF DEFINED(D2F_DEBUG)}false{$ELSE}false{$ENDIF};
 
-// hooks for player
-procedure g_Holmes_plrView (viewPortX, viewPortY, viewPortW, viewPortH: Integer);
-procedure g_Holmes_plrLaser (ax0, ay0, ax1, ay1: Integer);
+  var
+    g_ol_nice: Boolean = false;
+    g_ol_fill_walls: Boolean = false;
+    g_ol_rlayer_back: Boolean = false;
+    g_ol_rlayer_step: Boolean = false;
+    g_ol_rlayer_wall: Boolean = false;
+    g_ol_rlayer_door: Boolean = false;
+    g_ol_rlayer_acid1: Boolean = false;
+    g_ol_rlayer_acid2: Boolean = false;
+    g_ol_rlayer_water: Boolean = false;
+    g_ol_rlayer_fore: Boolean = false;
 
+  var
+    monMarkedUID: Integer = -1;
+    platMarkedGUID: Integer = -1;
 
 implementation
 
 uses
-  SysUtils, GL,
-  g_options;
+  {mempool,}
+  e_log, e_input,
+  g_player, g_monsters,
+  g_map, g_triggers, g_game, g_panel, g_console,
+  {xprofiler,}
+  fui_common, fui_events, fui_ctls,
+  {$IFDEF ENABLE_RENDER}
+    r_render,
+  {$ENDIF}
+  {rttiobj,} typinfo, e_res,
+  SysUtils, Classes,
+  {$IFDEF USE_SDL2}
+    SDL2,
+  {$ENDIF}
+  MAPDEF, g_options,
+  hashtable, xparser;
 
 
 var
   //globalInited: Boolean = false;
-  msX: Integer = -666;
-  msY: Integer = -666;
   msB: Word = 0; // button state
   kbS: Word = 0; // keyboard modifiers state
 
 
 // ////////////////////////////////////////////////////////////////////////// //
-// cursor (hi, Death Track!)
-const curWidth = 17;
-const curHeight = 23;
-
-const cursorImg: array[0..curWidth*curHeight-1] of Byte = (
-  0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
-  0,0,3,2,0,0,0,0,0,0,0,0,0,0,0,0,0,
-  1,0,3,2,2,0,0,0,0,0,0,0,0,0,0,0,0,
-  1,1,3,3,2,2,0,0,0,0,0,0,0,0,0,0,0,
-  1,1,3,3,4,2,2,0,0,0,0,0,0,0,0,0,0,
-  1,1,3,3,4,4,2,2,0,0,0,0,0,0,0,0,0,
-  1,1,3,3,4,4,4,2,2,0,0,0,0,0,0,0,0,
-  1,1,3,3,4,4,4,4,2,2,0,0,0,0,0,0,0,
-  1,1,3,3,4,4,4,5,6,2,2,0,0,0,0,0,0,
-  1,1,3,3,4,4,5,6,7,5,2,2,0,0,0,0,0,
-  1,1,3,3,4,5,6,7,5,4,5,2,2,0,0,0,0,
-  1,1,3,3,5,6,7,5,4,5,6,7,2,2,0,0,0,
-  1,1,3,3,6,7,5,4,5,6,7,7,7,2,2,0,0,
-  1,1,3,3,7,5,4,5,6,7,7,7,7,7,2,2,0,
-  1,1,3,3,5,4,5,6,8,8,8,8,8,8,8,8,2,
-  1,1,3,3,4,5,6,3,8,8,8,8,8,8,8,8,8,
-  1,1,3,3,5,6,3,3,1,1,1,1,1,1,1,0,0,
-  1,1,3,3,6,3,3,1,1,1,1,1,1,1,1,0,0,
-  1,1,3,3,3,3,0,0,0,0,0,0,0,0,0,0,0,
-  1,1,3,3,3,0,0,0,0,0,0,0,0,0,0,0,0,
-  1,1,3,3,0,0,0,0,0,0,0,0,0,0,0,0,0,
-  1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
-  1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
-);
-const cursorPal: array[0..9*4-1] of Byte = (
-    0,  0,  0,  0,
-    0,  0,  0,163,
-   85,255,255,255,
-   85, 85,255,255,
-  255, 85, 85,255,
-  170,  0,170,255,
-   85, 85, 85,255,
-    0,  0,  0,255,
-    0,  0,170,255
-);
+{$INCLUDE g_holmes.inc}
+
+
+// ////////////////////////////////////////////////////////////////////////// //
+{$INCLUDE g_holmes_cmd.inc}
+
+
+// ////////////////////////////////////////////////////////////////////////// //
+
+{$IF NOT DEFINED(ENABLE_RENDER)}
+function pmsCurMapX (): Integer; inline;
+begin
+  result := round(msX/g_dbg_scale)
+end;
+
+function pmsCurMapY (): Integer; inline;
+begin
+  result := round(msY/g_dbg_scale)
+end;
 
+function r_Render_HolmesViewIsSet (): Boolean;
+begin
+  result := false
+end;
+{$ENDIF}
 
+
+// ////////////////////////////////////////////////////////////////////////// //
 var
-  curtexid: GLuint = 0;
+  winHelp: TUITopWindow = nil;
+  winOptions: TUITopWindow = nil;
+  winLayers: TUITopWindow = nil;
+  winOutlines: TUITopWindow = nil;
+
+
+procedure createHelpWindow (); forward;
+procedure createOptionsWindow (); forward;
+procedure createLayersWindow (); forward;
+procedure createOutlinesWindow (); forward;
+
+
+procedure toggleLayersWindowCB (me: TUIControl);
+begin
+  showLayersWindow := not showLayersWindow;
+  if showLayersWindow then
+  begin
+    if (winLayers = nil) then createLayersWindow();
+    uiAddWindow(winLayers);
+  end
+  else
+  begin
+    uiRemoveWindow(winLayers);
+  end;
+end;
+
+procedure toggleOutlineWindowCB (me: TUIControl);
+begin
+  showOutlineWindow := not showOutlineWindow;
+  if showOutlineWindow then
+  begin
+    if (winOutlines = nil) then createOutlinesWindow();
+    uiAddWindow(winOutlines);
+  end
+  else
+  begin
+    uiRemoveWindow(winOutlines);
+  end;
+end;
+
+
+procedure createHelpWindow ();
+  procedure addHelpEmptyLine ();
+  var
+    stx: TUIStaticText;
+  begin
+    stx := TUIStaticText.Create();
+    stx.flExpand := true;
+    stx.halign := 0; // center
+    stx.text := '';
+    stx.header := false;
+    stx.line := false;
+    winHelp.appendChild(stx);
+  end;
+
+  procedure addHelpCaptionLine (const txt: AnsiString);
+  var
+    stx: TUIStaticText;
+  begin
+    stx := TUIStaticText.Create();
+    stx.flExpand := true;
+    stx.halign := 0; // center
+    stx.text := txt;
+    stx.header := true;
+    stx.line := true;
+    winHelp.appendChild(stx);
+  end;
+
+  procedure addHelpCaption (const txt: AnsiString);
+  var
+    stx: TUIStaticText;
+  begin
+    stx := TUIStaticText.Create();
+    stx.flExpand := true;
+    stx.halign := 0; // center
+    stx.text := txt;
+    stx.header := true;
+    stx.line := false;
+    winHelp.appendChild(stx);
+  end;
+
+  procedure addHelpKeyMouse (const key, txt, grp: AnsiString);
+  var
+    box: TUIHBox;
+    span: TUISpan;
+    stx: TUIStaticText;
+  begin
+    box := TUIHBox.Create();
+    box.flExpand := true;
+      // key
+      stx := TUIStaticText.Create();
+      stx.flExpand := true;
+      stx.halign := 1; // right
+      stx.valign := 0; // center
+      stx.text := key;
+      stx.header := true;
+      stx.line := false;
+      stx.flHGroup := grp;
+      box.appendChild(stx);
+      // span
+      span := TUISpan.Create();
+      span.flDefaultSize := TLaySize.Create(12, 1);
+      span.flExpand := true;
+      box.appendChild(span);
+      // text
+      stx := TUIStaticText.Create();
+      stx.flExpand := true;
+      stx.halign := -1; // left
+      stx.valign := 0; // center
+      stx.text := txt;
+      stx.header := false;
+      stx.line := false;
+      box.appendChild(stx);
+    winHelp.appendChild(box);
+  end;
+
+  procedure addHelpKey (const key, txt: AnsiString); begin addHelpKeyMouse(key, txt, 'help-keys'); end;
+  procedure addHelpMouse (const key, txt: AnsiString); begin addHelpKeyMouse(key, txt, 'help-mouse'); end;
 
-procedure createCursorTexture ();
 var
-  tex, tpp: PByte;
-  c: Integer;
-  x, y: Integer;
+  slist: array of AnsiString = nil;
+  cmd: PHolmesCommand;
+  bind: THolmesBinding;
+  f: Integer;
+  {
+  llb: TUISimpleText;
+  maxkeylen: Integer;
+  s: AnsiString;
+  }
 begin
-  if (curtexid <> 0) then exit; //begin glDeleteTextures(1, @curtexid); curtexid := 0; end;
+  winHelp := TUITopWindow.Create('Holmes Help');
+  winHelp.escClose := true;
+  winHelp.flHoriz := false;
 
-  GetMem(tex, curWidth*curHeight*4);
+  // keyboard
+  for cmd in cmdlist do cmd.helpmark := false;
 
-  tpp := tex;
-  for y := 0 to curHeight-1 do
+  //maxkeylen := 0;
+  for bind in keybinds do
   begin
-    for x := 0 to curWidth-1 do
+    if (Length(bind.key) = 0) then continue;
+    if cmdlist.get(bind.cmdName, cmd) then
     begin
-      c := cursorImg[y*curWidth+x]*4;
-      tpp^ := cursorPal[c+0]; Inc(tpp);
-      tpp^ := cursorPal[c+1]; Inc(tpp);
-      tpp^ := cursorPal[c+2]; Inc(tpp);
-      tpp^ := cursorPal[c+3]; Inc(tpp);
+      if (Length(cmd.help) > 0) then
+      begin
+        cmd.helpmark := true;
+        //if (maxkeylen < Length(bind.key)) then maxkeylen := Length(bind.key);
+      end;
     end;
   end;
 
-  glGenTextures(1, @curtexid);
-  if (curtexid = 0) then raise Exception.Create('can''t create Holmes texture');
+  for cmd in cmdlist do
+  begin
+    if not cmd.helpmark then continue;
+    if (Length(cmd.help) = 0) then begin cmd.helpmark := false; continue; end;
+    f := 0;
+    while (f < Length(slist)) and (CompareText(slist[f], cmd.section) <> 0) do Inc(f);
+    if (f = Length(slist)) then
+    begin
+      SetLength(slist, Length(slist)+1);
+      slist[High(slist)] := cmd.section;
+    end;
+  end;
 
-  glBindTexture(GL_TEXTURE_2D, curtexid);
-  glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
-  glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
-  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
-  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+  addHelpCaptionLine('KEYBOARD');
+  //llb := TUISimpleText.Create(0, 0);
+  for f := 0 to High(slist) do
+  begin
+    //if (f > 0) then llb.appendItem('');
+    if (f > 0) then addHelpEmptyLine();
+    //llb.appendItem(slist[f], true, true);
+    addHelpCaption(slist[f]);
+    for cmd in cmdlist do
+    begin
+      if not cmd.helpmark then continue;
+      if (CompareText(cmd.section, slist[f]) <> 0) then continue;
+      for bind in keybinds do
+      begin
+        if (Length(bind.key) = 0) then continue;
+        if (cmd.name = bind.cmdName) then
+        begin
+          //s := bind.key;
+          //while (Length(s) < maxkeylen) do s += ' ';
+          //s := '  '+s+' -- '+cmd.help;
+          //llb.appendItem(s);
+          addHelpMouse(bind.key, cmd.help);
+        end;
+      end;
+    end;
+  end;
+
+  // mouse
+  for cmd in cmdlist do cmd.helpmark := false;
 
-  //GLfloat[4] bclr = 0.0;
-  //glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, bclr.ptr);
+  //maxkeylen := 0;
+  for bind in msbinds do
+  begin
+    if (Length(bind.key) = 0) then continue;
+    if cmdlist.get(bind.cmdName, cmd) then
+    begin
+      if (Length(cmd.help) > 0) then
+      begin
+        cmd.helpmark := true;
+        //if (maxkeylen < Length(bind.key)) then maxkeylen := Length(bind.key);
+      end;
+    end;
+  end;
+
+  //llb.appendItem('');
+  //llb.appendItem('mouse', true, true);
+  if (f > 0) then addHelpEmptyLine();
+  addHelpCaptionLine('MOUSE');
+  for bind in msbinds do
+  begin
+    if (Length(bind.key) = 0) then continue;
+    if cmdlist.get(bind.cmdName, cmd) then
+    begin
+      if (Length(cmd.help) > 0) then
+      begin
+        //s := bind.key;
+        //while (Length(s) < maxkeylen) do s += ' ';
+        //s := '  '+s+' -- '+cmd.help;
+        //llb.appendItem(s);
+        addHelpKey(bind.key, cmd.help);
+      end;
+    end;
+  end;
 
-  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, curWidth, curHeight, 0, GL_RGBA{gltt}, GL_UNSIGNED_BYTE, tex);
+  //winHelp.appendChild(llb);
 
-  //FreeMem(tex);
+  uiLayoutCtl(winHelp);
+  winHelp.escClose := true;
+  winHelp.centerInScreen();
 end;
 
 
-procedure drawCursor ();
+procedure winLayersClosed (me: TUIControl); begin showLayersWindow := false; end;
+procedure winOutlinesClosed (me: TUIControl); begin showOutlineWindow := false; end;
+
+procedure addCheckBox (parent: TUIControl; const text: AnsiString; pvar: PBoolean; const aid: AnsiString='');
+var
+  cb: TUICheckBox;
+begin
+  cb := TUICheckBox.Create();
+  cb.flExpand := true;
+  cb.setVar(pvar);
+  cb.text := text;
+  cb.id := aid;
+  parent.appendChild(cb);
+end;
+
+procedure addButton (parent: TUIControl; const text: AnsiString; cb: TUIControl.TActionCB);
+var
+  but: TUIButton;
 begin
-  if (curtexid = 0) then createCursorTexture() else glBindTexture(GL_TEXTURE_2D, curtexid);
-  // blend it
-  glEnable(GL_BLEND);
-  glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
-  glEnable(GL_TEXTURE_2D);
-  // color and opacity
-  glColor4f(1, 1, 1, 0.9);
-  Dec(msX, 2);
-  glBegin(GL_QUADS);
-    glTexCoord2f(0.0, 0.0); glVertex2i(msX, msY); // top-left
-    glTexCoord2f(1.0, 0.0); glVertex2i(msX+curWidth, msY); // top-right
-    glTexCoord2f(1.0, 1.0); glVertex2i(msX+curWidth, msY+curHeight); // bottom-right
-    glTexCoord2f(0.0, 1.0); glVertex2i(msX, msY+curHeight); // bottom-left
-  glEnd();
-  Inc(msX, 2);
-  glDisable(GL_BLEND);
-  glDisable(GL_TEXTURE_2D);
-  glColor4f(1, 1, 1, 1);
-  glBindTexture(GL_TEXTURE_2D, 0);
+  but := TUIButton.Create();
+  //but.flExpand := true;
+  but.actionCB := cb;
+  but.text := text;
+  parent.appendChild(but);
 end;
 
 
-// ////////////////////////////////////////////////////////////////////////// //
-procedure g_Holmes_VidModeChanged ();
+procedure actionFillWalls (cb: TUIControl);
 begin
-  e_WriteLog(Format('Inspector: videomode changed: %dx%d', [gScreenWidth, gScreenHeight]), MSG_NOTIFY);
-  curtexid := 0; // texture is possibly lost here, idc
-  //createCursorTexture();
+  TUICheckBox(cb).checked := not TUICheckBox(cb).checked;
+  TUICheckBox(cb.topLevel['cbcontour']).enabled := not TUICheckBox(cb).checked;
 end;
 
-procedure g_Holmes_WindowFocused ();
+procedure createLayersWindow ();
+var
+  box: TUIVBox;
 begin
-  msB := 0;
-  kbS := 0;
+  winLayers := TUITopWindow.Create('layers');
+  winLayers.flHoriz := false;
+  winLayers.x0 := 10;
+  winLayers.y0 := 10;
+  winLayers.flHoriz := false;
+  winLayers.escClose := true;
+  winLayers.closeCB := winLayersClosed;
+
+  box := TUIVBox.Create();
+    addCheckBox(box, '~background', @g_rlayer_back);
+    addCheckBox(box, '~steps', @g_rlayer_step);
+    addCheckBox(box, '~walls', @g_rlayer_wall);
+    addCheckBox(box, '~doors', @g_rlayer_door);
+    addCheckBox(box, 'acid~1', @g_rlayer_acid1);
+    addCheckBox(box, 'acid~2', @g_rlayer_acid2);
+    addCheckBox(box, 'wate~r', @g_rlayer_water);
+    addCheckBox(box, '~foreground', @g_rlayer_fore);
+  winLayers.appendChild(box);
+
+  uiLayoutCtl(winLayers);
 end;
 
-procedure g_Holmes_WindowBlured ();
+
+procedure createOutlinesWindow ();
+var
+  box: TUIVBox;
 begin
+  winOutlines := TUITopWindow.Create('outlines');
+  winOutlines.flHoriz := false;
+  winOutlines.x0 := 100;
+  winOutlines.y0 := 30;
+  winOutlines.flHoriz := false;
+  winOutlines.escClose := true;
+  winOutlines.closeCB := winOutlinesClosed;
+
+  box := TUIVBox.Create();
+  box.hasFrame := true;
+  box.caption := 'layers';
+    addCheckBox(box, '~background', @g_ol_rlayer_back);
+    addCheckBox(box, '~steps', @g_ol_rlayer_step);
+    addCheckBox(box, '~walls', @g_ol_rlayer_wall);
+    addCheckBox(box, '~doors', @g_ol_rlayer_door);
+    addCheckBox(box, 'acid~1', @g_ol_rlayer_acid1);
+    addCheckBox(box, 'acid~2', @g_ol_rlayer_acid2);
+    addCheckBox(box, 'wate~r', @g_ol_rlayer_water);
+    addCheckBox(box, '~foreground', @g_ol_rlayer_fore);
+  winOutlines.appendChild(box);
+
+  box := TUIVBox.Create();
+  box.hasFrame := true;
+  box.caption := 'options';
+    addCheckBox(box, 'fi~ll walls', @g_ol_fill_walls, 'cbfill');
+    addCheckBox(box, 'con~tours', @g_ol_nice, 'cbcontour');
+  winOutlines.appendChild(box);
+
+  winOutlines.setActionCBFor('cbfill', actionFillWalls);
+
+  uiLayoutCtl(winOutlines);
 end;
 
 
-// ////////////////////////////////////////////////////////////////////////// //
+procedure createOptionsWindow ();
 var
-  vpSet: Boolean = false;
-  vpx, vpy: Integer;
-  vpw, vph: Integer;
-  laserSet: Boolean = false;
-  laserX0, laserY0, laserX1, laserY1: Integer;
-  monMarkedUID: Integer = -1;
-
-procedure g_Holmes_plrView (viewPortX, viewPortY, viewPortW, viewPortH: Integer);
+  box: TUIBox;
+  span: TUISpan;
 begin
-  vpSet := true;
-  vpx := viewPortX;
-  vpy := viewPortY;
-  vpw := viewPortW;
-  vph := viewPortH;
+  winOptions := TUITopWindow.Create('Holmes Options');
+  winOptions.flHoriz := false;
+  winOptions.flHoriz := false;
+  winOptions.escClose := true;
+
+  box := TUIVBox.Create();
+  box.hasFrame := true;
+  box.caption := 'visual';
+    addCheckBox(box, 'map ~grid', @showGrid);
+    addCheckBox(box, 'cursor ~position on map', @showMapCurPos);
+    addCheckBox(box, '~monster info', @showMonsInfo);
+    addCheckBox(box, 'monster LO~S to player', @showMonsLOS2Plr);
+    addCheckBox(box, 'monster ~cells (SLOW!)', @showAllMonsCells);
+    addCheckBox(box, 'draw ~triggers (SLOW!)', @showTriggers);
+  winOptions.appendChild(box);
+
+  box := TUIHBox.Create();
+  box.hasFrame := true;
+  box.caption := 'windows';
+  box.captionAlign := 0;
+  box.flAlign := 0;
+    addButton(box, '~layers', toggleLayersWindowCB);
+    span := TUISpan.Create();
+      span.flExpand := true;
+      span.flDefaultSize := TLaySize.Create(4, 1);
+      box.appendChild(span);
+    addButton(box, '~outline', toggleOutlineWindowCB);
+  winOptions.appendChild(box);
+
+  uiLayoutCtl(winOptions);
+  winOptions.centerInScreen();
 end;
 
-procedure g_Holmes_plrLaser (ax0, ay0, ax1, ay1: Integer);
+
+procedure toggleLayersWindow (arg: Integer=-1);
 begin
-  laserSet := true;
-  laserX0 := ax0;
-  laserY0 := ay0;
-  laserX1 := ax1;
-  laserY1 := ay1;
+  if (arg < 0) then showLayersWindow := not showLayersWindow else showLayersWindow := (arg > 0);
+  showLayersWindow := not showLayersWindow; // hack for callback
+  toggleLayersWindowCB(nil);
 end;
 
+procedure toggleOutlineWindow (arg: Integer=-1);
+begin
+  if (arg < 0) then showOutlineWindow := not showOutlineWindow else showOutlineWindow := (arg > 0);
+  showOutlineWindow := not showOutlineWindow; // hack for callback
+  toggleOutlineWindowCB(nil);
+end;
 
-function pmsCurMapX (): Integer; inline; begin result := msX+vpx; end;
-function pmsCurMapY (): Integer; inline; begin result := msY+vpy; end;
+procedure toggleHelpWindow (arg: Integer=-1);
+begin
+  if (winHelp = nil) then
+  begin
+    if (arg = 0) then exit;
+    createHelpWindow();
+  end;
+       if (arg < 0) then begin if not uiVisibleWindow(winHelp) then uiAddWindow(winHelp) else uiRemoveWindow(winHelp); end
+  else if (arg = 0) then begin if uiVisibleWindow(winHelp) then uiRemoveWindow(winHelp); end
+  else begin if (not uiVisibleWindow(winHelp)) then uiAddWindow(winHelp); end;
+  if (not uiVisibleWindow(winHelp)) then FreeAndNil(winHelp);
+end;
+
+procedure toggleOptionsWindow (arg: Integer=-1);
+begin
+  if (winOptions = nil) then createOptionsWindow();
+       if (arg < 0) then begin if not uiVisibleWindow(winOptions) then uiAddWindow(winOptions) else uiRemoveWindow(winOptions); end
+  else if (arg = 0) then begin if uiVisibleWindow(winOptions) then uiRemoveWindow(winOptions); end
+  else begin if not uiVisibleWindow(winOptions) then uiAddWindow(winOptions); end
+end;
 
 
-procedure plrDebugMouse (var ev: THMouseEvent);
+// ////////////////////////////////////////////////////////////////////////// //
+{$IFDEF USE_SDL2}
+procedure onKeyEvent (var ev: TFUIEvent);
+{$IF DEFINED(D2F_DEBUG)}
+var
+  pan: TPanel;
+  ex, ey: Integer;
+  dx, dy: Integer;
+{$ENDIF}
 
-  function wallToggle (pan: TPanel; tag: Integer): Boolean;
+  procedure dummyWallTrc (cx, cy: Integer);
   begin
-    result := false; // don't stop
-    if pan.Enabled then g_Map_DisableWall(pan.arrIdx) else g_Map_EnableWall(pan.arrIdx);
   end;
 
-  function monsAtDump (mon: TMonster; tag: Integer): Boolean;
+begin
+  // press
+  if (ev.press) then
   begin
-    result := false; // don't stop
-    e_WriteLog(Format('monster #%d; UID=%d', [mon.arrIdx, mon.UID]), MSG_NOTIFY);
-    monMarkedUID := mon.UID;
-    //if pan.Enabled then g_Map_DisableWall(pan.arrIdx) else g_Map_EnableWall(pan.arrIdx);
+    {$IF DEFINED(D2F_DEBUG)}
+    // C-UP, C-DOWN, C-LEFT, C-RIGHT: trace 10 pixels from cursor in the respective direction
+    if ((ev.scan = SDL_SCANCODE_UP) or (ev.scan = SDL_SCANCODE_DOWN) or (ev.scan = SDL_SCANCODE_LEFT) or (ev.scan = SDL_SCANCODE_RIGHT)) and
+       ((ev.kstate and TFUIEvent.ModCtrl) <> 0) then
+    begin
+      ev.eat();
+      dx := pmsCurMapX;
+      dy := pmsCurMapY;
+      case ev.scan of
+        SDL_SCANCODE_UP: dy -= 120;
+        SDL_SCANCODE_DOWN: dy += 120;
+        SDL_SCANCODE_LEFT: dx -= 120;
+        SDL_SCANCODE_RIGHT: dx += 120;
+      end;
+      {$IF DEFINED(D2F_DEBUG)}
+      //mapGrid.dbgRayTraceTileHitCB := dummyWallTrc;
+      mapGrid.dbgShowTraceLog := true;
+      {$ENDIF}
+      pan := g_Map_traceToNearest(pmsCurMapX, pmsCurMapY, dx, dy, (GridTagWall or GridTagDoor or GridTagStep or GridTagAcid1 or GridTagAcid2 or GridTagWater), @ex, @ey);
+      {$IF DEFINED(D2F_DEBUG)}
+      //mapGrid.dbgRayTraceTileHitCB := nil;
+      mapGrid.dbgShowTraceLog := false;
+      {$ENDIF}
+      e_LogWritefln('v-trace: (%d,%d)-(%d,%d); end=(%d,%d); hit=%d', [pmsCurMapX, pmsCurMapY, dx, dy, ex, ey, (pan <> nil)]);
+      exit;
+    end;
+    {$ENDIF}
   end;
+end;
+{$ELSE}
+procedure onKeyEvent (var ev: TFUIEvent);
+begin
+end;
+{$ENDIF}
+
 
+// ////////////////////////////////////////////////////////////////////////// //
+procedure g_Holmes_OnEvent (var ev: TFUIEvent);
+  var doeat: Boolean = false;
 begin
-  //e_WriteLog(Format('mouse: x=%d; y=%d; but=%d; bstate=%d', [msx, msy, but, bstate]), MSG_NOTIFY);
-  if (gPlayer1 = nil) then exit;
-  if (ev.kind <> THMouseEvent.Press) then exit;
+  if g_Game_IsNet then exit;
+  if not g_holmes_enabled then exit;
+  if g_holmes_imfunctional then exit;
+
+  holmesInitCommands();
+  holmesInitBinds();
 
-  if (ev.but = THMouseEvent.Left) then
+  msB := ev.bstate;
+  kbS := ev.kstate;
+
+  if (ev.key) then
   begin
-    mapGrid.forEachAtPoint(pmsCurMapX, pmsCurMapY, wallToggle, (GridTagWall or GridTagDoor));
-    exit;
+{$IFDEF USE_SDL2}
+    case ev.scan of
+      SDL_SCANCODE_LCTRL, SDL_SCANCODE_RCTRL,
+      SDL_SCANCODE_LALT, SDL_SCANCODE_RALT,
+      SDL_SCANCODE_LSHIFT, SDL_SCANCODE_RSHIFT:
+        doeat := true;
+    end;
+{$ENDIF}
+  end
+  else if (ev.mouse) then
+  begin
+    msX := ev.x;
+    msY := ev.y;
+    msB := ev.bstate;
+    kbS := ev.kstate;
+    msB := msB;
   end;
 
-  if (ev.but = THMouseEvent.Right) then
+  uiDispatchEvent(ev);
+  if (not ev.alive) then exit;
+
+  if (ev.mouse) then
   begin
-    monMarkedUID := -1;
-    e_WriteLog('===========================', MSG_NOTIFY);
-    monsGrid.forEachAtPoint(pmsCurMapX, pmsCurMapY, monsAtDump);
-    e_WriteLog('---------------------------', MSG_NOTIFY);
-    exit;
+    if (gPlayer1 <> nil) and r_Render_HolmesViewIsSet() then msbindExecute(ev);
+    ev.eat();
+  end
+  else
+  begin
+    if keybindExecute(ev) then ev.eat();
+    if (ev.alive) then onKeyEvent(ev);
   end;
+
+  if (doeat) then ev.eat();
 end;
 
 
-procedure plrDebugDraw ();
+// ////////////////////////////////////////////////////////////////////////// //
+procedure bcOneMonsterThinkStep (); begin gmon_debug_think := false; gmon_debug_one_think_step := true; end;
+procedure bcOneMPlatThinkStep (); begin g_dbgpan_mplat_active := false; g_dbgpan_mplat_step := true; end;
+procedure bcMPlatToggle (); begin g_dbgpan_mplat_active := not g_dbgpan_mplat_active; end;
+
+procedure bcToggleMonsterInfo (arg: Integer=-1); begin if (arg < 0) then showMonsInfo := not showMonsInfo else showMonsInfo := (arg > 0); end;
+procedure bcToggleMonsterLOSPlr (arg: Integer=-1); begin if (arg < 0) then showMonsLOS2Plr := not showMonsLOS2Plr else showMonsLOS2Plr := (arg > 0); end;
+procedure bcToggleMonsterCells (arg: Integer=-1); begin if (arg < 0) then showAllMonsCells := not showAllMonsCells else showAllMonsCells := (arg > 0); end;
+procedure bcToggleDrawTriggers (arg: Integer=-1); begin if (arg < 0) then showTriggers := not showTriggers else showTriggers := (arg > 0); end;
 
-  function monsCollector (mon: TMonster; tag: Integer): Boolean;
-  var
-    ex, ey: Integer;
-    mx, my, mw, mh: Integer;
+procedure bcToggleCurPos (arg: Integer=-1); begin if (arg < 0) then showMapCurPos := not showMapCurPos else showMapCurPos := (arg > 0); end;
+procedure bcToggleGrid (arg: Integer=-1); begin if (arg < 0) then showGrid := not showGrid else showGrid := (arg > 0); end;
+
+procedure bcMonsterSpawn (s: AnsiString);
+var
+  mon: TMonster;
+begin
+  if not gGameOn or g_Game_IsClient then
   begin
-    result := false;
-    mon.getMapBox(mx, my, mw, mh);
-    e_DrawQuad(mx, my, mx+mw-1, my+mh-1, 255, 255, 0, 96);
-    if lineAABBIntersects(laserX0, laserY0, laserX1, laserY1, mx, my, mw, mh, ex, ey) then
-    begin
-      e_DrawPoint(8, ex, ey, 0, 255, 0);
-    end;
+    conwriteln('cannot spawn monster in this mode');
+    exit;
   end;
+  mon := g_Mons_SpawnAt(s, pmsCurMapX, pmsCurMapY);
+  if (mon = nil) then begin conwritefln('unknown monster id: ''%s''', [s]); exit; end;
+  monMarkedUID := mon.UID;
+end;
 
+procedure bcMonsterWakeup ();
 var
   mon: TMonster;
-  mx, my, mw, mh: Integer;
 begin
-  //e_DrawPoint(4, plrMouseX, plrMouseY, 255, 0, 255);
-  if (gPlayer1 = nil) then exit;
+  if (monMarkedUID <> -1) then
+  begin
+    mon := g_Monsters_ByUID(monMarkedUID);
+    if (mon <> nil) then mon.WakeUp();
+  end;
+end;
 
-  //e_WriteLog(Format('(%d,%d)-(%d,%d)', [laserX0, laserY0, laserX1, laserY1]), MSG_NOTIFY);
+procedure bcPlayerTeleport ();
+var
+  x, y, w, h: Integer;
+begin
+  //e_WriteLog(Format('TELEPORT: (%d,%d)', [pmsCurMapX, pmsCurMapY]), MSG_NOTIFY);
+  if (gPlayers[0] <> nil) then
+  begin
+    gPlayers[0].getMapBox(x, y, w, h);
+    gPlayers[0].TeleportTo(pmsCurMapX-w div 2, pmsCurMapY-h div 2, true, 69); // 69: don't change dir
+  end;
+end;
 
-  glPushMatrix();
-  glTranslatef(-vpx, -vpy, 0);
+procedure dbgToggleTraceBox (arg: Integer=-1); begin if (arg < 0) then showTraceBox := not showTraceBox else showTraceBox := (arg > 0); end;
 
-  g_Mons_AlongLine(laserX0, laserY0, laserX1, laserY1, monsCollector, true);
+procedure dbgToggleHolmesPause (arg: Integer=-1); begin if (arg < 0) then g_Game_HolmesPause(not gPauseHolmes) else g_Game_HolmesPause(arg > 0); end;
 
-  if (monMarkedUID <> -1) then
+procedure cbAtcurSelectMonster ();
+  function monsAtDump (mon: TMonster{; tag: Integer}): Boolean;
   begin
-    mon := g_Monsters_ByUID(monMarkedUID);
-    if (mon <> nil) then
+    result := true; // stop
+    e_WriteLog(Format('monster #%d (UID:%u) (proxyid:%d)', [mon.arrIdx, mon.UID, mon.proxyId]), TMsgType.Notify);
+    monMarkedUID := mon.UID;
+    dumpPublishedProperties(mon);
+  end;
+var
+  plr: TPlayer;
+  x, y, w, h: Integer;
+  mit: PMonster;
+  it: TMonsterGrid.Iter;
+begin
+  monMarkedUID := -1;
+  if (Length(gPlayers) > 0) then
+  begin
+    plr := gPlayers[0];
+    if (plr <> nil) then
     begin
-      mon.getMapBox(mx, my, mw, mh);
-      e_DrawQuad(mx, my, mx+mw-1, my+mh-1, 255, 0, 0, 30);
+      plr.getMapBox(x, y, w, h);
+      if (pmsCurMapX >= x) and (pmsCurMapY >= y) and (pmsCurMapX < x+w) and (pmsCurMapY < y+h) then
+      begin
+        dumpPublishedProperties(plr);
+      end;
     end;
   end;
-
-  //e_DrawPoint(16, laserX0, laserY0, 255, 255, 255);
-
-  glPopMatrix();
+  //e_WriteLog('===========================', MSG_NOTIFY);
+  it := monsGrid.forEachAtPoint(pmsCurMapX, pmsCurMapY);
+  for mit in it do monsAtDump(mit^);
+  it.release();
+  //e_WriteLog('---------------------------', MSG_NOTIFY);
 end;
 
+procedure cbAtcurDumpMonsters ();
+  function monsAtDump (mon: TMonster{; tag: Integer}): Boolean;
+  begin
+    result := false; // don't stop
+    e_WriteLog(Format('monster #%d (UID:%u) (proxyid:%d)', [mon.arrIdx, mon.UID, mon.proxyId]), TMsgType.Notify);
+  end;
+var
+  mit: PMonster;
+  it: TMonsterGrid.Iter;
+begin
+  it := monsGrid.forEachAtPoint(pmsCurMapX, pmsCurMapY);
+  if (it.length > 0) then
+  begin
+    e_WriteLog('===========================', TMsgType.Notify);
+    for mit in it do monsAtDump(mit^);
+    e_WriteLog('---------------------------', TMsgType.Notify);
+  end;
+  it.release();
+end;
 
-{
-    procedure drawTileGrid ();
-    var
-      x, y: Integer;
+procedure cbAtcurDumpWalls ();
+  function wallToggle (pan: TPanel{; tag: Integer}): Boolean;
+  begin
+    result := false; // don't stop
+    if (platMarkedGUID = -1) then platMarkedGUID := pan.guid;
+    e_LogWritefln('wall ''%s'' #%d(%d); enabled=%d (%d); (%d,%d)-(%d,%d)', [pan.mapId, pan.arrIdx, pan.proxyId, Integer(pan.Enabled), Integer(mapGrid.proxyEnabled[pan.proxyId]), pan.X, pan.Y, pan.Width, pan.Height]);
+    dumpPublishedProperties(pan);
+  end;
+var
+  hasTrigs: Boolean = false;
+  f: Integer;
+  trig: PTrigger;
+  mwit: PPanel;
+  it: TPanelGrid.Iter;
+begin
+  platMarkedGUID := -1;
+  it := mapGrid.forEachAtPoint(pmsCurMapX, pmsCurMapY, (GridTagWall or GridTagDoor));
+  if (it.length > 0) then
+  begin
+    e_WriteLog('=== TOGGLE WALL ===', TMsgType.Notify);
+    for mwit in it do wallToggle(mwit^);
+    e_WriteLog('--- toggle wall ---', TMsgType.Notify);
+  end;
+  it.release();
+  if showTriggers then
+  begin
+    for f := 0 to High(gTriggers) do
     begin
-      y := mapGrid.gridY0;
-      while (y < mapGrid.gridY0+mapGrid.gridHeight) do
+      trig := @gTriggers[f];
+      if (pmsCurMapX >= trig.x) and (pmsCurMapY >= trig.y) and (pmsCurMapX < trig.x+trig.width) and (pmsCurMapY < trig.y+trig.height) then
       begin
-        x := mapGrid.gridX0;
-        while (x < mapGrid.gridX0+mapGrid.gridWidth) do
-        begin
-          if (x+mapGrid.tileSize > vpx) and (y+mapGrid.tileSize > vpy) and
-             (x < vpx+vpw) and (y < vpy+vph) then
-          begin
-            e_DrawQuad(x, y, x+mapGrid.tileSize-1, y+mapGrid.tileSize-1, 96, 96, 96, 96);
-          end;
-          Inc(x, mapGrid.tileSize);
-        end;
-        Inc(y, mapGrid.tileSize);
+        if not hasTrigs then begin writeln('=== TRIGGERS ==='); hasTrigs := true; end;
+        writeln('trigger ''', trig.mapId, ''' of type ''', trigType2Str(trig.TriggerType), '''');
       end;
     end;
-}
-
+    if hasTrigs then writeln('--- triggers ---');
+  end;
+end;
 
-// ////////////////////////////////////////////////////////////////////////// //
-function g_Holmes_mouseEvent (var ev: THMouseEvent): Boolean;
+procedure cbAtcurToggleWalls ();
+  function wallToggle (pan: TPanel{; tag: Integer}): Boolean;
+  begin
+    result := false; // don't stop
+    //e_WriteLog(Format('wall #%d(%d); enabled=%d (%d); (%d,%d)-(%d,%d)', [pan.arrIdx, pan.proxyId, Integer(pan.Enabled), Integer(mapGrid.proxyEnabled[pan.proxyId]), pan.X, pan.Y, pan.Width, pan.Height]), MSG_NOTIFY);
+    if pan.Enabled then g_Map_DisableWallGUID(pan.guid) else g_Map_EnableWallGUID(pan.guid);
+  end;
+var
+  mwit: PPanel;
+  it: TPanelGrid.Iter;
 begin
-  result := true;
-  msX := ev.x;
-  msY := ev.y;
-  msB := ev.bstate;
-  kbS := ev.kstate;
-  plrDebugMouse(ev);
+  //e_WriteLog('=== TOGGLE WALL ===', MSG_NOTIFY);
+  //e_WriteLog('--- toggle wall ---', MSG_NOTIFY);
+  it := mapGrid.forEachAtPoint(pmsCurMapX, pmsCurMapY, (GridTagWall or GridTagDoor));
+  for mwit in it do wallToggle(mwit^);
+  it.release();
 end;
 
 
-function g_Holmes_KeyEvent (var ev: THKeyEvent): Boolean;
+// ////////////////////////////////////////////////////////////////////////// //
+procedure holmesInitCommands ();
 begin
-  result := false;
+  if (cmdlist <> nil) then exit;
+  cmdAdd('win_layers', toggleLayersWindow, 'toggle layers window', 'window control');
+  cmdAdd('win_outline', toggleOutlineWindow, 'toggle outline window', 'window control');
+  cmdAdd('win_help', toggleHelpWindow, 'toggle help window', 'window control');
+  cmdAdd('win_options', toggleOptionsWindow, 'toggle options window', 'window control');
+
+  cmdAdd('mon_think_step', bcOneMonsterThinkStep, 'one monster think step', 'monster control');
+  cmdAdd('mon_info', bcToggleMonsterInfo, 'toggle monster info', 'monster control');
+  cmdAdd('mon_los_plr', bcToggleMonsterLOSPlr, 'toggle monster LOS to player', 'monster control');
+  cmdAdd('mon_cells', bcToggleMonsterCells, 'toggle "show all cells occupied by monsters" (SLOW!)', 'monster control');
+  cmdAdd('mon_wakeup', bcMonsterWakeup, 'wake up selected monster', 'monster control');
+
+  cmdAdd('mon_spawn', bcMonsterSpawn, 'spawn monster', 'monster control');
+
+  cmdAdd('mplat_step', bcOneMPlatThinkStep, 'one mplat think step', 'mplat control');
+  cmdAdd('mplat_toggle', bcMPlatToggle, 'activate/deactivate moving platforms', 'mplat control');
+
+  cmdAdd('plr_teleport', bcPlayerTeleport, 'teleport player', 'player control');
+
+  cmdAdd('dbg_curpos', bcToggleCurPos, 'toggle "show cursor position on the map"', 'various');
+  cmdAdd('dbg_grid', bcToggleGrid, 'toggle grid', 'various');
+  cmdAdd('dbg_triggers', bcToggleDrawTriggers, 'show/hide triggers (SLOW!)', 'various');
+
+  cmdAdd('atcur_select_monster', cbAtcurSelectMonster, 'select monster to operate', 'monster control');
+  cmdAdd('atcur_dump_monsters', cbAtcurDumpMonsters, 'dump monsters in cell', 'monster control');
+  cmdAdd('atcur_dump_walls', cbAtcurDumpWalls, 'dump walls in cell', 'wall control');
+  cmdAdd('atcur_disable_walls', cbAtcurToggleWalls, 'disable walls', 'wall control');
+
+  cmdAdd('dbg_tracebox', dbgToggleTraceBox, 'test traceBox()', 'player control');
+
+  cmdAdd('hlm_pause', dbgToggleHolmesPause, '"Holmes" pause mode', 'game control');
 end;
 
 
-// ////////////////////////////////////////////////////////////////////////// //
-procedure g_Holmes_Draw ();
+procedure holmesInitBinds ();
+var
+  st: TStream = nil;
+  pr: TTextParser = nil;
+  s, kn, v: AnsiString;
+  kmods: Byte;
+  mbuts: Byte;
 begin
-  glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); // modify color buffer
-  glDisable(GL_STENCIL_TEST);
-  glDisable(GL_BLEND);
-  glDisable(GL_SCISSOR_TEST);
-  glDisable(GL_TEXTURE_2D);
+  kbS := kbS;
+  if not keybindsInited then
+  begin
+    // keyboard
+    keybindAdd('F1', 'win_help');
+    keybindAdd('M-F1', 'win_options');
+    keybindAdd('C-O', 'win_outline');
+    keybindAdd('C-L', 'win_layers');
+
+    keybindAdd('M-M', 'mon_think_step');
+    keybindAdd('M-I', 'mon_info');
+    keybindAdd('M-L', 'mon_los_plr');
+    keybindAdd('M-G', 'mon_cells');
+    keybindAdd('M-A', 'mon_wakeup');
+
+    keybindAdd('M-P', 'mplat_step');
+    keybindAdd('M-O', 'mplat_toggle');
+
+    keybindAdd('C-T', 'plr_teleport');
+    keybindAdd('M-T', 'dbg_tracebox');
+
+    keybindAdd('C-P', 'dbg_curpos');
+    keybindAdd('C-G', 'dbg_grid');
+    keybindAdd('C-X', 'dbg_triggers');
+
+    keybindAdd('C-1', 'mon_spawn zombie');
+
+    keybindAdd('C-S-P', 'hlm_pause');
+
+    // mouse
+    msbindAdd('LMB', 'atcur_select_monster');
+    msbindAdd('M-LMB', 'atcur_dump_monsters');
+    msbindAdd('RMB', 'atcur_dump_walls');
+    msbindAdd('M-RMB', 'atcur_disable_walls');
+
+    // load bindings from file
+    try
+      st := e_OpenResourceRO(ConfigDirs, 'holmes.rc');
+      pr := TFileTextParser.Create(st);
+      conwriteln('parsing "holmes.rc"...');
+      while (pr.tokType <> pr.TTEOF) do
+      begin
+        s := pr.expectId();
+             if (s = 'stop') then break
+        else if (s = 'unbind_keys') then keybinds := nil
+        else if (s = 'unbind_mouse') then msbinds := nil
+        else if (s = 'bind') then
+        begin
+               if (pr.tokType = pr.TTStr) then s := pr.expectStr(false)
+          else if (pr.tokType = pr.TTInt) then s := Format('%d', [pr.expectInt()])
+          else s := pr.expectId();
 
-  plrDebugDraw();
+               if (pr.tokType = pr.TTStr) then v := pr.expectStr(false)
+          else if (pr.tokType = pr.TTInt) then v := Format('%d', [pr.expectInt()])
+          else v := pr.expectId();
 
-  drawCursor();
+          kn := parseModKeys(s, kmods, mbuts);
+          if (CompareText(kn, 'lmb') = 0) or (CompareText(kn, 'rmb') = 0) or (CompareText(kn, 'mmb') = 0) or (CompareText(kn, 'None') = 0) then
+          begin
+            msbindAdd(s, v);
+          end
+          else
+          begin
+            keybindAdd(s, v);
+          end;
+        end;
+      end;
+    except on e: Exception do // sorry
+      if (pr <> nil) then conwritefln('Holmes config parse error at (%s,%s): %s', [pr.tokLine, pr.tokCol, e.message]);
+    end;
+    if (pr <> nil) then pr.Free() else st.Free(); // ownership
+  end;
 end;
 
 
+begin
+  // shut up, fpc!
+  msB := msB;
+
+  fuiEventCB := g_Holmes_OnEvent;
+  //uiContext.font := 'win14';
 end.