diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt index fcd2c14500..2542964911 100644 --- a/apps/openmw/CMakeLists.txt +++ b/apps/openmw/CMakeLists.txt @@ -31,7 +31,7 @@ add_openmw_dir (mwgui confirmationdialog alchemywindow referenceinterface spellwindow mainmenu quickkeysmenu itemselection spellbuyingwindow loadingscreen levelupdialog waitdialog spellcreationdialog enchantingdialog trainingwindow travelwindow imagebutton exposedwindow cursor spellicons - merchantrepair repair soulgemdialog companionwindow bookpage + merchantrepair repair soulgemdialog companionwindow bookpage journalviewmodel ) add_openmw_dir (mwdialogue diff --git a/apps/openmw/mwgui/journalviewmodel.cpp b/apps/openmw/mwgui/journalviewmodel.cpp new file mode 100644 index 0000000000..89be5292fb --- /dev/null +++ b/apps/openmw/mwgui/journalviewmodel.cpp @@ -0,0 +1,472 @@ +#include "journalviewmodel.hpp" + +#include "../mwbase/world.hpp" +#include "../mwbase/journal.hpp" +#include "../mwbase/environment.hpp" +#include "../mwdialogue/journalentry.hpp" + +//#include "MyGUI_LanguageManager.h" + +#include + +#include +#include + +using namespace MWGui; + +namespace MWGui { struct JournalViewModel; } + +static void injectMonthName (std::ostream & os, int month); + +template +class keyword_search +{ +public: + + typedef typename string_t::const_iterator point; + + struct match + { + point Beg; + point End; + value_t Value; + }; + + void seed (string_t Keyword, value_t Value) + { + seed_impl (std::move (Keyword), std::move (Value), 0, Root); + } + + void clear () + { + Root.Children.clear (); + Root.Keyword.clear (); + } + + bool search (point Beg, point End, match & Match) + { + for (auto i = Beg; i != End; ++i) + { + // check first character + auto candidate = Root.Children.find (std::tolower (*i, Locale)); + + // no match, on to next character + if (candidate == Root.Children.end ()) + continue; + + // see how far the match goes + auto j = i; + + while ((j + 1) != End) + { + auto next = candidate->second.Children.find (std::tolower (*++j, Locale)); + + if (next == candidate->second.Children.end ()) + break; + + candidate = next; + } + + // didn't match enough to disambiguate, on to next character + if (!candidate->second.Keyword.size ()) + continue; + + // match the rest of the keyword + auto t = candidate->second.Keyword.begin () + (j - i); + + while (j != End && t != candidate->second.Keyword.end ()) + { + if (std::tolower (*j, Locale) != std::tolower (*t, Locale)) + break; + + ++j, ++t; + } + + // didn't match full keyword, on to next character + if (t != candidate->second.Keyword.end ()) + continue; + + // we did it, report the good news + Match.Value = candidate->second.Value; + Match.Beg = i; + Match.End = j; + + return true; + } + + // no match in range, report the bad news + return false; + } + +private: + + struct entry + { + typedef std::map childen_t; + + string_t Keyword; + value_t Value; + childen_t Children; + }; + + void seed_impl (string_t Keyword, value_t Value, size_t Depth, entry & Entry) + { + auto ch = tolower (Keyword.at (Depth), Locale); + + auto j = Entry.Children.find (ch); + + if (j == Entry.Children.end ()) + { + Entry.Children [ch].Value = std::move (Value); + Entry.Children [ch].Keyword = std::move (Keyword); + } + else + { + if (j->second.Keyword.size () > 0) + { + if (Keyword == j->second.Keyword) + throw std::runtime_error ("duplicate keyword inserted"); + + auto pushValue = std::move (j->second.Value); + auto pushKeyword = std::move (j->second.Keyword); + + j->second.Keyword.clear (); + + if (Depth >= pushKeyword.size ()) + throw std::runtime_error ("unexpected"); + + seed_impl (std::move (pushKeyword), std::move (pushValue), Depth+1, j->second); + } + + seed_impl (std::move (Keyword), std::move (Value), Depth+1, j->second); + } + + } + + entry Root; + std::locale Locale; +}; + +struct MWGui::JournalViewModel : IJournalViewModel +{ + typedef keyword_search foobar; + + mutable bool FooBar_loaded; + mutable foobar FooBar; + + std::locale Locale; + + JournalViewModel () + { + FooBar_loaded = false; + } + + virtual ~JournalViewModel () + { + } + + //TODO: replace this nasty BS + static utf8_span to_utf8_span (std::string const & str) + { + if (str.size () == 0) + return utf8_span (utf8_point (NULL), utf8_point (NULL)); + + utf8_point point = reinterpret_cast (&str.front ()); + + return utf8_span (point, point + str.size ()); + } + + void load () + { + } + + void unload () + { + FooBar.clear (); + FooBar_loaded = false; + } + + void ensure_FooBar_loaded () const + { + if (!FooBar_loaded) + { + auto journal = MWBase::Environment::get().getJournal(); + + for(auto i = journal->topicBegin(); i != journal->topicEnd (); ++i) + FooBar.seed (i->first, intptr_t (&i->second)); + + FooBar_loaded = true; + } + } + + wchar_t tolower (wchar_t ch) const { return std::tolower (ch, Locale); } + + bool is_empty () const + { + auto journal = MWBase::Environment::get().getJournal(); + + return journal->begin () == journal->end (); + } + + template + struct base_entry : IInterface + { + iterator_t itr; + JournalViewModel const * Model; + + base_entry (JournalViewModel const * Model, iterator_t itr) : + Model (Model), itr (itr), loaded (false) + {} + + virtual ~base_entry () {} + + mutable bool loaded; + mutable std::string utf8text; + + virtual std::string getText () const = 0; + + void ensure_loaded () const + { + if (!loaded) + { + utf8text = getText (); + loaded = true; + } + } + + utf8_span body () const + { + ensure_loaded (); + + return to_utf8_span (utf8text); + } + + void visit_spans (std::function < void (topic_id, size_t, size_t)> visitor) const + { + ensure_loaded (); + Model->ensure_FooBar_loaded (); + + std::string::const_iterator i = utf8text.begin (); + + foobar::match Match; + + while (i != utf8text.end () && Model->FooBar.search (i, utf8text.end (), Match)) + { + if (i != Match.Beg) + visitor (0, i - utf8text.begin (), Match.Beg - utf8text.begin ()); + + visitor (Match.Value, Match.Beg - utf8text.begin (), Match.End - utf8text.begin ()); + + i = Match.End; + } + + if (i != utf8text.end ()) + visitor (0, i - utf8text.begin (), utf8text.size ()); + } + + }; + + void visit_quest_names (bool active_only, std::function visitor) const + { + auto journal = MWBase::Environment::get ().getJournal (); + + for (auto i = journal->questBegin (); i != journal->questEnd (); ++i) + { + if (active_only && i->second.isFinished ()) + continue; + + visitor (reinterpret_cast (&i->second), to_utf8_span (i->first)); + } + } + + void visit_quest_name (quest_id questId, std::function visitor) const + { + MWDialogue::Quest const * quest = reinterpret_cast (questId); + + auto name = quest->getName (); + + visitor (to_utf8_span (name)); + } + + template + struct journal_entry : base_entry + { + using base_entry ::itr; + + mutable std::string timestamp_buffer; + + journal_entry (JournalViewModel const * Model, iterator_t itr) : + base_entry (Model, itr) + {} + + std::string getText () const + { + return itr->getText(MWBase::Environment::get().getWorld()->getStore()); + } + + utf8_span timestamp () const + { + if (timestamp_buffer.empty ()) + { + std::ostringstream os; + + os << itr->mDayOfMonth << ' '; + + injectMonthName (os, itr->mMonth); + + os << " (Day " << (itr->mDay + 1) << ')'; + + timestamp_buffer = os.str (); + } + + return to_utf8_span (timestamp_buffer); + } + }; + + void visit_journal_entries (quest_id questId, std::function visitor) const + { + auto journal = MWBase::Environment::get().getJournal(); + + if (questId != 0) + { + MWDialogue::Quest const * quest = reinterpret_cast (questId); + + for(auto i = journal->begin(); i != journal->end (); ++i) + { + for (auto j = quest->begin (); j != quest->end (); ++j) + { + if (i->mInfoId == *j) + visitor (journal_entry (this, i)); + } + } + } + else + { + for(auto i = journal->begin(); i != journal->end (); ++i) + visitor (journal_entry (this, i)); + } + } + + void visit_topics (std::function visitor) const + { + throw std::runtime_error ("not implemented"); + } + + void visit_topic_name (topic_id topicId, std::function visitor) const + { + auto & Topic = * reinterpret_cast (topicId); + + visitor (to_utf8_span (Topic.getName ())); + } + + void visit_topic_names_starting_with (int character, std::function < void (topic_id , utf8_span) > visitor) const + { + auto journal = MWBase::Environment::get().getJournal(); + + for (auto i = journal->topicBegin (); i != journal->topicEnd (); ++i) + { + if (i->first [0] != std::tolower (character, Locale)) + continue; + + visitor (topic_id (&i->second), to_utf8_span (i->first)); + } + + } + + void visit_topic_entries (topic_id topicId, std::function visitor) const + { + auto & Topic = * reinterpret_cast (topicId); + + for (auto i = Topic.begin (); i != Topic.end (); ++i) + { + typedef decltype (Topic.begin()) iterator_t; + + struct entry : base_entry + { + MWDialogue::Topic const & Topic; + + mutable std::string source_buffer; + + + entry (JournalViewModel const * Model, MWDialogue::Topic const & Topic, iterator_t itr) : + base_entry (Model, itr), Topic (Topic) + {} + + std::string getText () const + { + return Topic.getEntry (*itr).getText(MWBase::Environment::get().getWorld()->getStore()); + + } + + utf8_span source () const + { + if (source_buffer.empty ()) + source_buffer = "someone"; + return to_utf8_span (source_buffer); + } + + }; + + visitor (entry (this, Topic, i)); + } + } +}; + +static void injectMonthName (std::ostream & os, int month) +{ +#ifndef MYGUI_SIGNLETON_FIXED_FOR_MSVC + static char const * month_names [] = + { + "Morning Star", + "Sun's Dawn", + "First Seed", + "Rain's Hand", + "Second Seed", + "Midyear", + "Sun's Height", + "Last Seed", + "Hearthfire", + "Frostfall", + "Sun's Dusk", + "Evening Star", + }; + if (month >= 0 && month <= 11) + os << month_names [month ]; + else + os << month ; +#else + auto lm = MyGUI::LanguageManager::getInstance(); + + if (month == 0) + os << lm.getTag ("sMonthMorningstar"); + else if (month == 1) + os << lm.getTag ("sMonthSunsdawn"); + else if (month == 2) + os << lm.getTag ("sMonthFirstseed"); + else if (month == 3) + os << lm.getTag ("sMonthRainshand"); + else if (month == 4) + os << lm.getTag ("sMonthSecondseed"); + else if (month == 5) + os << lm.getTag ("sMonthMidyear"); + else if (month == 6) + os << lm.getTag ("sMonthSunsheight"); + else if (month == 7) + os << lm.getTag ("sMonthLastseed"); + else if (month == 8) + os << lm.getTag ("sMonthHeartfire"); + else if (month == 9) + os << lm.getTag ("sMonthFrostfall"); + else if (month == 10) + os << lm.getTag ("sMonthSunsdusk"); + else if (month == 11) + os << lm.getTag ("sMonthEveningstar"); + else + os << month; +#endif +} + +IJournalViewModel::ptr IJournalViewModel::create () +{ + return std::make_shared (); +} diff --git a/apps/openmw/mwgui/journalviewmodel.hpp b/apps/openmw/mwgui/journalviewmodel.hpp new file mode 100644 index 0000000000..fa5a601286 --- /dev/null +++ b/apps/openmw/mwgui/journalviewmodel.hpp @@ -0,0 +1,91 @@ +#ifndef MWGUI_JOURNALVIEWMODEL_HPP +#define MWGUI_JOURNALVIEWMODEL_HPP + +#include +#include +#include +#include + +namespace MWGui +{ + /// View-Model for the journal GUI + /// + /// This interface defines an abstract data model suited + // specifically to the needs of the journal GUI. It isolates + /// the journal GUI from the implementation details of the + /// game data store. + struct IJournalViewModel + { + typedef std::shared_ptr ptr; + + typedef intptr_t quest_id; + typedef intptr_t topic_id; + typedef uint8_t const * utf8_point; + typedef std::pair utf8_span; + + /// The base interface for both journal entries and topics. + struct IEntry + { + /// returns the body text for the journal entry + /// + /// This function returns a borrowed reference to the body of the + /// journal entry. The returned reference becomes invalid when the + /// entry is destroyed. + virtual utf8_span body () const = 0; + + /// Visits each subset of text in the body, delivering the beginning + /// and end of the span relative to the body, and a valid topic ID if + /// the span represents a keyword, or zero if not. + virtual void visit_spans (std::function visitor) const = 0; + }; + + /// An interface to topic data. + struct ITopicEntry : IEntry + { + /// Returns a pre-formatted span of UTF8 encoded text representing + /// the name of the NPC this portion of dialog was heard from. + virtual utf8_span source () const = 0; + }; + + /// An interface to journal data. + struct IJournalEntry : IEntry + { + /// Returns a pre-formatted span of UTF8 encoded text representing + /// the in-game date this entry was added to the journal. + virtual utf8_span timestamp () const = 0; + }; + + + /// called prior to journal opening + virtual void load () = 0; + + /// called prior to journal closing + virtual void unload () = 0; + + /// returns true if their are no journal entries to display + virtual bool is_empty () const = 0; + + /// provides access to the name of the quest with the specified identifier + virtual void visit_quest_name (topic_id topicId, std::function visitor) const = 0; + + /// walks the active and optionally completed, quests providing the quest id and name + virtual void visit_quest_names (bool active_only, std::function visitor) const = 0; + + /// walks over the journal entries related to the specified quest identified by its id + virtual void visit_journal_entries (quest_id questId, std::function visitor) const = 0; + + /// provides the name of the topic specified by its id + virtual void visit_topic_name (topic_id topicId, std::function visitor) const = 0; + + /// walks over the topics whose names start with the specified character providing the topics id and name + virtual void visit_topic_names_starting_with (int character, std::function < void (topic_id , utf8_span) > visitor) const = 0; + + /// walks over the topic entries for the topic specified by its identifier + virtual void visit_topic_entries (topic_id topicId, std::function visitor) const = 0; + + // create an instance of the default journal view model implementation + static ptr create (); + }; +} + +#endif // MWGUI_JOURNALVIEWMODEL_HPP