Peoplemon  0.1.0
Peoplemon 3 game source documentation
ConversationWindow.cpp
Go to the documentation of this file.
2 
3 #include <BLIB/Scripts.hpp>
4 #include <Core/Items/Id.hpp>
5 #include <Core/Properties.hpp>
6 #include <queue>
7 
8 namespace editor
9 {
10 namespace component
11 {
12 using namespace bl::gui;
13 namespace
14 {
15 const std::string EmptyFile = "<no file selected>";
16 
17 std::string makePath(const std::string& local) {
18  return bl::util::FileUtil::joinPath(core::Properties::ConversationPath(), local);
19 }
20 
21 const core::file::Conversation DefaultConversation = []() {
23  core::file::Conversation::Node node(core::file::Conversation::Node::Type::Talk);
24  node.next() = 9999999;
25  val.appendNode(node);
26  return val;
27 }();
28 
29 bool nodeTerminates(const std::vector<core::file::Conversation::Node>& nodes, unsigned int i) {
30  std::queue<unsigned int> toVisit;
31  std::vector<bool> visited(nodes.size(), false);
32 
33  toVisit.push(i);
34  visited[i] = true;
35 
36  std::vector<unsigned int> next;
37  next.reserve(8);
38  while (!toVisit.empty()) {
39  const unsigned int n = toVisit.front();
40  toVisit.pop();
41 
43  for (unsigned int j : next) {
44  if (j >= nodes.size()) return true;
45 
46  if (!visited[j]) {
47  visited[j] = true;
48  toVisit.push(j);
49  }
50  }
51  }
52 
53  return false;
54 }
55 
56 } // namespace
57 
58 ConversationWindow::ConversationWindow(const SelectCb& onSelect, const CancelCb& onCancel)
59 : selectCb(onSelect)
60 , cancelCb(onCancel)
61 , value(DefaultConversation)
62 , currentNode(0)
63 , nodeBox(Box::create(LinePacker::create(LinePacker::Vertical, 4.f)))
64 , nodeComponent([this](bool f) { window->setForceFocus(f); },
65  [this]() {
66  makeDirty();
67  value.setNode(currentNode, nodeComponent.getValue());
68  },
69  [this]() {
70  if (value.nodes().size() > 1 &&
71  1 == bl::dialog::tinyfd_messageBox(
72  "Warning", "Delete conversation node?", "yesno", "warning", 0)) {
73  value.deleteNode(currentNode);
74  if (currentNode >= value.nodes().size()) {
75  currentNode = value.nodes().size() - 1;
76  }
77  makeDirty();
78  window->queueUpdateAction(std::bind(&ConversationWindow::sync, this));
79  }
80  },
81  [this]() { treeComponent->update(value.nodes()); },
82  [this](unsigned int current) {
83  core::file::Conversation::Node node{core::file::Conversation::Node::Type::Talk};
84  node.nextOnReject() = 999999;
85  node.nextOnPass() = current;
86  value.appendNode(node);
87  return value.nodes().size() - 1;
88  },
89  std::bind(&ConversationWindow::setSelected, this, std::placeholders::_1), nodeBox,
90  &value.nodes())
91 , filePicker(
93  [this](const std::string& c) {
94  std::string conv = c;
95  if (bl::util::FileUtil::getExtension(conv) !=
97  filePickerMode != FilePickerMode::OpenExisting) {
99  }
100 
101  filePicker.close();
102  fileLabel->setText(conv);
103  window->setForceFocus(true);
104  switch (filePickerMode) {
105  case FilePickerMode::MakeNew:
106  value = DefaultConversation;
107  currentNode = 0;
108  window->queueUpdateAction(std::bind(&ConversationWindow::sync, this));
109  makeClean();
110  break;
111  case FilePickerMode::OpenExisting:
112  currentNode = 0;
113  if (!value.load(makePath(conv))) {
114  const std::string msg = "Failed to load conversation: " + conv;
115  bl::dialog::tinyfd_messageBox("Error", msg.c_str(), "ok", "error", 1);
116  value = DefaultConversation;
117  fileLabel->setText(EmptyFile);
118  }
119  makeClean();
120  window->queueUpdateAction(std::bind(&ConversationWindow::sync, this));
121  break;
122  case FilePickerMode::SetFile:
123  default:
124  makeDirty();
125  break;
126  }
127  },
128  [this]() {
129  filePicker.close();
130  window->setForceFocus(true);
131  })
132 , dirty(false) {
133  window = Window::create(LinePacker::create(LinePacker::Vertical), "Conversation Editor");
134  window->getSignal(Event::Closed).willAlwaysCall([this](const Event&, Element*) {
135  window->remove();
136  window->setForceFocus(false);
137  cancelCb();
138  });
139 
140  Box::Ptr row = Box::create(LinePacker::create(LinePacker::Horizontal, 4.f));
141  Button::Ptr but = Button::create("New");
142  but->getSignal(Event::LeftClicked).willAlwaysCall([this](const Event&, Element*) {
143  filePickerMode = FilePickerMode::MakeNew;
144  if (!dirty || confirmDiscard()) {
145  window->setForceFocus(false);
146  filePicker.open(FilePicker::CreateNew, "New Conversation", parent, true);
147  }
148  });
149  row->pack(but, false, true);
150  but = Button::create("Open");
151  but->getSignal(Event::LeftClicked).willAlwaysCall([this](const Event&, Element*) {
152  filePickerMode = FilePickerMode::OpenExisting;
153  if (!dirty || confirmDiscard()) {
154  window->setForceFocus(false);
155  filePicker.open(FilePicker::PickExisting, "Open Conversation", parent, true);
156  }
157  });
158  row->pack(but, false, true);
159  but = Button::create("Set File");
160  but->setTooltip("Change the file to save to");
161  but->getSignal(Event::LeftClicked).willAlwaysCall([this](const Event&, Element*) {
162  filePickerMode = FilePickerMode::SetFile;
163  window->setForceFocus(false);
164  filePicker.open(FilePicker::CreateNew, "Change File", parent, true);
165  });
166  row->pack(but, false, true);
167  saveBut = Button::create("Save");
168  saveBut->getSignal(Event::LeftClicked).willAlwaysCall([this](const Event&, Element*) {
169  if (validate()) {
170  if (value.save(makePath(fileLabel->getText()))) { makeClean(); }
171  else { bl::dialog::tinyfd_messageBox("Error", "Failed to save", "ok", "error", 1); }
172  }
173  });
174  row->pack(saveBut, false, true);
175  fileLabel = Label::create(EmptyFile);
176  fileLabel->setColor(sf::Color::Cyan, sf::Color::Transparent);
177  row->pack(fileLabel, true, true);
178  window->pack(row, true, false);
179 
180  treeComponent = ConversationTree::create(
181  std::bind(&ConversationWindow::setSelected, this, std::placeholders::_1));
182  treeComponent->setRequisition({500.f, 500.f});
183 
184  row = Box::create(LinePacker::create(LinePacker::Horizontal, 8.f));
185  row->pack(treeComponent, true, true);
186  nodeBox->setRequisition({270.f, 500.f});
187  nodeBox->setColor(sf::Color::Transparent, sf::Color::Black);
188  nodeBox->setOutlineThickness(2.f);
189  row->pack(nodeBox, true, true);
190  window->pack(row, true, true);
191 
192  row = Box::create(LinePacker::create(
193  LinePacker::Horizontal, 0.f, LinePacker::Compact, LinePacker::RightAlign));
194  but = Button::create("Use Conversation");
195  but->getSignal(Event::LeftClicked).willAlwaysCall([this](const Event&, Element*) {
196  if (validate()) {
197  if (!dirty || confirmDiscard()) {
198  window->setForceFocus(false);
199  window->remove();
200  selectCb(fileLabel->getText());
201  }
202  }
203  });
204  but->setColor(sf::Color(52, 235, 222), sf::Color::Black);
205  row->pack(but);
206  window->pack(row, true, false);
207 
208  window->setPosition({100.f, 100.f});
209 }
210 
211 void ConversationWindow::sync() {
212  nodeComponent.update(currentNode, value.nodes().at(currentNode));
213  treeComponent->update(value.nodes());
214  treeComponent->setSelected(currentNode);
215 }
216 
217 void ConversationWindow::open(bl::gui::GUI* p, const std::string& current) {
218  parent = p;
219  currentNode = 0;
220  if (current.empty() || !value.load(makePath(current))) { value = DefaultConversation; }
221 
222  nodeComponent.setParent(p);
223  sync();
224  fileLabel->setText(current);
225  makeClean();
226 
227  parent->pack(window);
228  window->setForceFocus(true);
229 }
230 
231 bool ConversationWindow::confirmDiscard() const {
232  return !dirty || 1 == bl::dialog::tinyfd_messageBox(
233  "Warning", "Discard unsaved changes?", "yesno", "warning", 0);
234 }
235 
236 bool ConversationWindow::validate() const {
237  const auto error = [](const std::string& e) {
238  bl::dialog::tinyfd_messageBox("Error", e.c_str(), "ok", "error", 1);
239  };
240 
241  const auto warning = [](const std::string& e) {
242  bl::dialog::tinyfd_messageBox("Warning", e.c_str(), "ok", "warning", 1);
243  };
244 
245  const auto checkNode = [&error, &warning](const core::file::Conversation::Node& n,
246  unsigned int i) -> bool {
248  using OutputCb = std::function<void(const std::string&)>;
249 
250  const auto output = [i](const OutputCb& cb, const std::string& m) {
251  std::string o = "Node " + std::to_string(i) + ": " + m;
252  for (unsigned int i = 0; i < o.size(); ++i) {
253  if (o[i] == '\'') o[i] = '`';
254  if (o[i] == '"') {
255  o.erase(i, 1);
256  o.insert(i, "<QUOTE>");
257  }
258  }
259  cb(o);
260  };
261 
262  switch (n.getType()) {
263  case T::Talk:
264  case T::Prompt:
265  if (n.message().empty()) { output(warning, "Message is empty"); }
266  if (n.getType() == T::Prompt) {
267  if (n.choices().empty()) {
268  output(error, "Prompt has no choices");
269  return false;
270  }
271  for (const auto& c : n.choices()) {
272  if (c.first.empty()) {
273  output(error, "Choice has empty text");
274  return false;
275  }
276  }
277  }
278  return true;
279 
280  case T::TakeItem:
281  case T::GiveItem:
282  if (n.item().id == core::item::Id::Unknown) {
283  output(error, "Invalid item");
284  return false;
285  }
286  return true;
287 
288  case T::GiveMoney:
289  case T::TakeMoney:
290  if (n.money() == 0) {
291  output(error, "Invalid money amount");
292  return false;
293  }
294  return true;
295 
296  case T::RunScript: {
297  bl::script::Script check(n.script());
298  if (!check.valid()) {
299  output(error, "SyntaxError: " + check.errorMessage());
300  return false;
301  }
302  }
303  return true;
304 
305  case T::SetSaveFlag:
306  case T::CheckSaveFlag:
307  if (n.saveFlag().empty()) {
308  output(error, "Save flag cannot be empty");
309  return false;
310  }
311  return true;
312 
313  case T::CheckInteracted:
314  return true;
315 
316  default:
317  output(error, "Unknown node type");
318  return false;
319  }
320  };
321 
322  unsigned int validated = 0;
323  std::queue<unsigned int> toVisit;
324  std::vector<bool> visited(value.nodes().size(), false);
325 
326  toVisit.push(0);
327  visited[0] = true;
328 
329  std::vector<unsigned int> next;
330  next.reserve(8);
331  while (!toVisit.empty()) {
332  const unsigned int n = toVisit.front();
333  const core::file::Conversation::Node& node = value.nodes()[n];
334  toVisit.pop();
335  validated += 1;
336 
337  if (!checkNode(node, n)) return false;
338  if (!nodeTerminates(value.nodes(), n)) {
339  error("Conversation end is unreachable from node " + std::to_string(n));
340  return false;
341  }
342 
344  if (next.empty()) {
345  error("Node " + std::to_string(n) + " has no jumps");
346  return false;
347  }
348 
349  for (unsigned int j : next) {
350  if (j >= value.nodes().size()) continue;
351  if (!visited[j]) {
352  visited[j] = true;
353  toVisit.push(j);
354  }
355  }
356  }
357 
358  if (validated != value.nodes().size()) {
359  warning("There are " + std::to_string(value.nodes().size() - validated) +
360  " unreachable nodes");
361  }
362 
363  return true;
364 }
365 
366 void ConversationWindow::makeClean() {
367  dirty = false;
368  saveBut->setColor(sf::Color::Green, sf::Color::Black);
369 }
370 
371 void ConversationWindow::makeDirty() {
372  dirty = true;
373  saveBut->setColor(sf::Color::Red, sf::Color::Black);
374 }
375 
376 void ConversationWindow::setSelected(unsigned int i) {
377  if (i >= value.nodes().size()) {
378  bl::dialog::tinyfd_messageBox("Error", "Invalid node", "ok", "error", 1);
379  }
380  else {
381  currentNode = i;
382  window->queueUpdateAction(
383  [this]() { nodeComponent.update(currentNode, value.nodes().at(currentNode)); });
384  treeComponent->setSelected(i);
385  }
386 }
387 
388 } // namespace component
389 } // namespace editor
All classes and functionality used for implementing the game editor.
Definition: Tile.hpp:11
Stores a conversation that an NPC or trainer can have with the player.
static void getNextJumps(const Node &node, std::vector< unsigned int > &jumps)
Populates the jumps vector with the indices reachable from the given node.
bool load(const std::string &file)
Loads the conversation from the given file.
const std::vector< Node > & nodes() const
Returns the list of nodes in the conversation.
bool save(const std::string &file) const
Saves the conversation to the given file.
void appendNode(const Node &node)
Appends the given node to the list of nodes.
Building block of conversations. A conversation is a tree of nodes and each node is an action that ha...
std::uint32_t & nextOnReject()
Returns the next node if a check is rejected (ie not taking money or item)
Type
Represents the different effects that nodes can have.
static const std::string & ConversationPath()
Definition: Properties.cpp:539
static const std::string & ConversationFileExtension()
Definition: Properties.cpp:545
void update(unsigned int i, const core::file::Conversation::Node &node)
Update the node editor content with the given node value.
void setParent(bl::gui::GUI *parent)
Set the parent GUI object.
static Ptr create(const ClickCb &clickCb)
Construct a new Conversation Tree component.
std::function< void(const std::string &)> SelectCb
Callback for when a conversation is chosen.
std::function< void()> CancelCb
Callback for when the window is closed with no choice made.
ConversationWindow(const SelectCb &onSelect, const CancelCb &onCancel)
Initialize a new conversation window.
void open(bl::gui::GUI *parent, const std::string &current)
Actually opens the window and initializes with a conversation.