Newer
Older
Import / applications / HighwayDash / ports / Framework / Dialog.cpp
//  BlockyFroggy
//  Copyright © 2017 John Ryland.
//  All rights reserved.
#include <string>
#include <sstream>
#include <vector>
#include "Dialog.h"
#include "ResourceLoader.h"
#include "Json.h"
#include "Lua.h"
#include "Log.h"
#include "Utilities.h"


DECLARE_LOG_CONTEXT(GUI)


using namespace GameUi;


bool Rectangle::isInside(Point pt) const
{
  return (pt.x >= x && pt.x <= (x+w) && pt.y >= y && pt.y <= (y+h));
}


uint8_t UpdateState::getTextureIdForAsset(const char* assetName)
{
  //! @todo remove these statics - something needs to take ownership of this
  static uint8_t nextId = 255;//0;
  static std::map<std::string,uint8_t> g_cachedTextureIds;
  if (g_cachedTextureIds.count(assetName))
    return g_cachedTextureIds[assetName];
  nextId++;
  Log(LL_Debug, "tex[%i] => %s", nextId, assetName);
  g_cachedTextureIds[assetName] = nextId;
  TextureLoadJob job = { std::string(assetName), nextId };
  m_loadTexturesQueue.push_back(job);
  //m_loadTexturesQueue.emplace_back(std::string(assetName), nextId);
  return nextId;
}


void DrawItems::addQuad(const Rectangle& r, const Rectangle& tex, Color col, uint8_t texId)
{
  int t[2][6] = { { 0, 0, 1, 1, 1, 0 }, { 0, 1, 1, 1, 0, 0 } };
  for (int i = 0; i < 6; i++)
    m_items.push_back(UiVertex{int16_t(r.x+t[0][i]*r.w), int16_t(r.y+t[1][i]*r.h), col.r,col.g,col.b,col.a,
        uint8_t(tex.x+t[0][i]*tex.w), uint8_t(tex.y+t[1][i]*tex.h), texId});
}


void DrawItems::addIcon(int x, int y, char icon, Color col, int scale)
{
  if (icon >= 0 && icon < 24)
  {
    int u = icon % 12;
    int v = icon / 12;
    v += 4;
    addQuad(Rectangle{x, y, 20*scale, 19*scale}, Rectangle{u*20, v*19, 20, 19}, col);
  }
}


void DrawItems::addChar(int x, int y, char ch, Color col, int scale)
{
  if ( ch >= 32 && ch < 127 ) {
    ch -= 32;
    int u = ch % 24;
    int v = ch / 24;
    addQuad(Rectangle{x, y, 10*scale, 19*scale}, Rectangle{u*10, v*19, 10, 19}, col);
  }
}


static void ReplaceStringInPlace(std::string& subject, const std::string& search, const std::string& replace)
{
  size_t pos = 0;
  while ((pos = subject.find(search, pos)) != std::string::npos) {
    subject.replace(pos, search.length(), replace);
    pos += replace.length();
  }
}


#if 0
thread_local std::string t_tmpSearch;
#endif

// Currently only does a specific type of variable expansion, but idea is it could do more
void Text::update(StringMap& variables)
{
  formatted = input;
  for(auto const &var : variables)
  {
#if 0
    t_tmpSearch = "${";
    t_tmpSearch += var.first;
    t_tmpSearch += "}";
    ReplaceStringInPlace(formatted, t_tmpSearch, var.second);
#else
    ReplaceStringInPlace(formatted, "${" + var.first + "}", var.second);
#endif
  }
  length = formatted.size();
}


void DrawItems::addString(int x, int y, const Text& a_text, Color col, int scale)
{
  const char* str = a_text.c_str();
  for (int i = 0; i < a_text.size(); i++)
    addChar(x + 10*scale*i, y, str[i], col, scale);
}


void DrawItems::addContrastString(int x, int y, const Text& a_text, int scale)
{
  int offs[5][3] = { {0,0,0}, {-1,1,0}, {-1,2,0}, {1,3,0}, {0,0,0xffffff} };
  for (int i = 0; i < 5; i++)
    addString(x+offs[i][0], y+offs[i][1], a_text, offs[i][2] | 0xff000000, scale);
}


void DrawItems::add9Slice(const Rectangle& r, const Rectangle& t, int radius, Color col)
{
  int tx[4] = { t.x, t.x+int(t.w*0.333333f), t.x+int(t.w*0.666666f), t.x+t.w };
  int ty[4] = { t.y, t.y+int(t.h*0.333333f), t.y+int(t.h*0.666666f), t.y+t.h };
  int x[4]  = { r.x, r.x + radius, r.x+r.w - radius, r.x+r.w };
  int y[4]  = { r.y, r.y + radius, r.y+r.h - radius, r.y+r.h };
  for (int j = 0; j < 3; j++)
    for (int i = 0; i < 3; i++)
      addQuad(Rectangle{x[i], y[j], x[i+1]-x[i], y[j+1]-y[j]}, 
        Rectangle{tx[i], ty[j], tx[i+1]-tx[i], ty[j+1]-ty[j]}, col);
}


bool UiControl::handleMouse(MouseState ms)
{
  m_mouseState = ms;
  bool inside = m_geometry.isInside(ms.pos);
  bool lastDown = m_mouseDownLast;
  m_mouseDownLast = ms.down;
  bool insideLast = m_mouseInsideLast;
  m_mouseInsideLast = inside;
  bool buttonStateChange = lastDown != ms.down;
  bool insideStateChange = insideLast != inside;
  if (buttonStateChange && !ms.down && m_mouseDownInside) {
    m_mouseDownInside = false;
    onMouseRelease();
    if (inside)
      onMouseClicked();
    return true;
  }
  if (buttonStateChange && ms.down && inside)
  {
    m_mouseDownInside = true;
    onMousePress();
    return true;
  }
  if (insideStateChange) {
    if (inside)
      onMouseEnter();
    else
      onMouseLeave();
    return false;
  }
  onMouseMove();
  return false;
}


void UiControl::genericDispatch(const char* event)
{
  if (m_ui && m_actionMap.count(event))
    for (auto action : m_actionMap[event])
      m_ui->dialogRedirect(action);
}


// Potentially this lives in game code
void Ui::dialogRedirect(std::string redirect)
{
  Log(LL_Debug, "redirecting to %s", redirect.c_str());
  std::vector<std::string> parts = Utilities::split(redirect, ':');
  if (!parts.size() || parts[0] == "")
    return;
  if (parts[0] == "close" || parts[0] == "pop") {
    Log(LL_Debug, "closing current");
    closeCurrent();
  } else if (parts[0] == "push") {
    Log(LL_Debug, "pushing %s", parts[1].c_str());
    if (m_dialogMap.count(parts[1])) {
      Dialog* d = m_dialogMap[parts[1]];
      pushDialog(d);
    }
  } else if (parts[0] == "exec") {
    Log(LL_Info, "executing lua function: %s", parts[1].c_str());
    m_luaVM.callLua(parts[1].c_str());
  } else if (parts[0] == "goto") {
    Log(LL_Debug, "going to %s", parts[1].c_str());
    if (m_dialogMap.count(parts[1])) {
      Dialog* d = m_dialogMap[parts[1]];
      closeCurrent();
      pushDialog(d);
    }
  }
}

static bool StringToBool(std::string str)
{
  return (str == "on" || str == "true" || str == "yes");
}

AnimationParameters AnimationParametersFromJson(ConstJsonValueRef animParams)
{
  AnimationParameters params;
  params.m_event      = animParams["event"]("show");
  params.m_parameter  = animParams["parameter"]("x");
  lookupCurveFunction(animParams["curve"]("linear"), params.m_curve);
  //params.m_curve    = animParams["curve"].GetString();
  params.m_timeScale  = animParams["timeScale"](1000);
  params.m_timeOffset = animParams["timeOffset"](0);
  params.m_loop       = StringToBool(animParams["loop"]("yes"));
  params.m_t0         = animParams["t0"](0);
  params.m_t1         = animParams["t1"](1);
  return params;
}


void Ui::genericDispatch(const char* event)
{
  if (m_gameFlowSteps.count(event))
    for (auto action : m_gameFlowSteps[event])
      dialogRedirect(action);
}


UiControl* ControlFromJson(ConstJsonValueRef control, LuaVM& m_luaVM)
{
  UiControl* newControl = nullptr;
  std::string childType = control["type"]("null");
  int x = control["x"](0);
  int y = control["y"](0);
  int w = control["w"](0);
  int h = control["h"](0);
  int r = control["r"](0);
  int g = control["g"](0);
  int b = control["b"](0);
  int a = control["a"](0);
  uint32_t col = (a << 24) | (b << 16) | (g << 8) | r;
  const char* text = control["text"]("default");

  AnimationList animationList;
  for (auto a : control["animations"])
    animationList.push_back(AnimationParametersFromJson(a));

  ActionMap actionMap;
  for (auto actions : control["actions"])
    for (auto action : actions)
      actionMap[actions.getName()].push_back(action("null"));

  if (childType == "Script") {
    m_luaVM.loadFile(control["lua"]("error.lua"));
  } else if (childType == "Label") {
    newControl = new Label(x, y, control["align"](0), text);
  } else if (childType == "Button") {
    newControl = new Button(x, y, text);
  } else if (childType == "ToolButton") {
    newControl = new ToolButton(x, y, control["icon"](0));
  } else if (childType == "CheckBox") {
    newControl = new CheckBox(x, y, text, control["binding"]("checkState"));
  } else if (childType == "Slider") {
    newControl = new Slider(x, y, h, control["binding"]("sliderValue"));
  } else if (childType == "HSlider") {
    newControl = new HSlider(x, y, w, control["binding"]("sliderValue"));
  } else if (childType == "Frame") {
    newControl = new Frame(x, y, w, h, text);
  } else if (childType == "Panel") {
    newControl = new Panel(w, h, col);// control["title"]("Title"), control["icon"](0));
  } else if (childType == "Rectangle") {
    newControl = new RectangleShape(x, y, w, h, col);
  } else if (childType == "Image") {
    newControl = new ImageShape(x, y, control["asset"]("null"));
  }

  if (newControl) {
    newControl->m_actionMap = actionMap;
    newControl->m_animationList = animationList;
    //newControl->m_ui = this; // no good doing it here, as dialog creates a control
    //newDialog->m_controls.push_back(newControl);
  }
  else
  {
    if (childType != "Script")
      Log(LL_Error, "Bad control in UI json, %s", childType.c_str());
  }
  return newControl;
}


void Ui::loadDialogsFromAsset(const char* assetName)
{
  loadFileAsync(assetName, true, [this](Resource* res)
  {
      rapidjson::Document jsonDoc;
      if (!res->data.data() || jsonDoc.Parse((char*)res->data.data()).HasParseError()) {
        Log(LL_Error, "json parse error of ui json");
        return;
      }

      for (auto actions : ConstJsonValueRef(jsonDoc["actions"]))
        for (auto action : actions)
          m_gameFlowSteps[actions.getName()].push_back(action("null"));

      // This is here to support reloading the dialog map (goes back to same screen again)
      std::string oldDialog = "";
      if (m_stack.size() && m_stack.back()) {
        oldDialog = m_stack.back()->m_name;
      }

      for (auto dialog : m_dialogMap)
        delete dialog.second;
      m_dialogMap.clear();

      for (auto di : ConstJsonValueRef(jsonDoc["dialogs"]))
      {
          Dialog *newDialog = new Dialog();

          for (auto bg : di["background"]) {
            UiControl *newBg = ControlFromJson(bg, m_luaVM);
            if (newBg) {
              newBg->m_ui = this;
              newDialog->m_background.push_back(newBg);
            }
          }

          for (auto view : di["views"]) {
            View *newView = new View(view["x"](0), view["y"](0), view["w"](0), view["h"](0), view["view"]("blank"));
            newDialog->m_views.push_back(newView);
          }

          for (auto control : di["controls"]) {
            UiControl *newControl = ControlFromJson(control, m_luaVM);
            if (newControl) {
              newDialog->m_controls.push_back(newControl);
            }
          }

          for (UiControl* ctrl : newDialog->m_controls)
            ctrl->m_ui = this; // doing here ensures all child controls have m_ui set
          // probably a better way to do this during construction

          ActionMap actionMap;
          for (auto actions : di["actions"])
            for (auto action : actions)
              actionMap[actions.getName()].push_back(action("null"));

          //Log(LL_Trace, "adding %s to dialog map", di->name.GetString());
          newDialog->m_actionMap = actionMap;
          newDialog->m_stage = di["stage"]("blank");
          newDialog->m_name = di.getName();
          m_dialogMap[di.getName()] = newDialog;
      }

      if (oldDialog == "")
      {
        genericDispatch("onLaunch");
      }
      else
      {
        if (m_dialogMap.count(oldDialog)) {
          pop();
          pushDialog(m_dialogMap[oldDialog]);
        } else {
          Log(LL_Error, "didn't find old dialog: %s", oldDialog.c_str());
        }
      }

  });
}